diff --git a/app/scrcpy.1 b/app/scrcpy.1
index 2cb75933..20b1e142 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -831,6 +831,14 @@ Turn on the camera torch (camera mode only)
.B MOD+Shift+t
Turn off the camera torch (camera mode only)
+.TP
+.B MOD+Up
+Zoom camera in (camera mode only)
+
+.TP
+.B MOD+Down
+Zoom camera out (camera mode only)
+
.SH Environment variables
diff --git a/app/src/cli.c b/app/src/cli.c
index 931b4db8..9c955fb5 100644
--- a/app/src/cli.c
+++ b/app/src/cli.c
@@ -1228,6 +1228,14 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+t" },
.text = "Turn off the camera torch (camera mode only)",
},
+ {
+ .shortcuts = { "MOD+Up" },
+ .text = "Zoom camera in (camera mode only)",
+ },
+ {
+ .shortcuts = { "MOD+Down" },
+ .text = "Zoom camera out (camera mode only)",
+ },
};
static const struct sc_envvar envvars[] = {
diff --git a/app/src/control_msg.c b/app/src/control_msg.c
index 8d74ea84..146fbed7 100644
--- a/app/src/control_msg.c
+++ b/app/src/control_msg.c
@@ -191,6 +191,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
+ case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
+ case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
// no additional data
return 1;
default:
@@ -325,6 +327,12 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
LOG_CMSG("camera set torch %s",
msg->camera_set_torch.on ? "on" : "off");
break;
+ case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
+ LOG_CMSG("camera zoom in");
+ break;
+ case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
+ LOG_CMSG("camera zoom out");
+ break;
default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break;
diff --git a/app/src/control_msg.h b/app/src/control_msg.h
index e1e9c5dc..36e9a4b6 100644
--- a/app/src/control_msg.h
+++ b/app/src/control_msg.h
@@ -44,6 +44,8 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
+ SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
+ SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
};
enum sc_copy_key {
diff --git a/app/src/input_manager.c b/app/src/input_manager.c
index 874843cc..633d134f 100644
--- a/app/src/input_manager.c
+++ b/app/src/input_manager.c
@@ -315,6 +315,30 @@ camera_set_torch(struct sc_input_manager *im, bool on) {
}
}
+static void
+camera_zoom_in(struct sc_input_manager *im) {
+ assert(im->controller && im->camera);
+
+ struct sc_control_msg msg;
+ msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN;
+
+ if (!sc_controller_push_msg(im->controller, &msg)) {
+ LOGW("Could not request camera zoom in");
+ }
+}
+
+static void
+camera_zoom_out(struct sc_input_manager *im) {
+ assert(im->controller && im->camera);
+
+ struct sc_control_msg msg;
+ msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT;
+
+ if (!sc_controller_push_msg(im->controller, &msg)) {
+ LOGW("Could not request camera zoom out");
+ }
+}
+
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@@ -601,6 +625,18 @@ sc_input_manager_process_key(struct sc_input_manager *im,
camera_set_torch(im, !shift);
}
return;
+ case SDLK_DOWN:
+ if (!shift && down && !paused) {
+ // forward repeated events
+ camera_zoom_out(im);
+ }
+ return;
+ case SDLK_UP:
+ if (!shift && down && !paused) {
+ // forward repeated events
+ camera_zoom_in(im);
+ }
+ return;
}
}
diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c
index c71a4570..bd445a1d 100644
--- a/app/tests/test_control_msg_serialize.c
+++ b/app/tests/test_control_msg_serialize.c
@@ -465,6 +465,36 @@ static void test_serialize_camera_set_torch(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
+static void test_serialize_camera_zoom_in(void) {
+ struct sc_control_msg msg = {
+ .type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
+ };
+
+ uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
+ size_t size = sc_control_msg_serialize(&msg, buf);
+ assert(size == 1);
+
+ const uint8_t expected[] = {
+ SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
+ };
+ assert(!memcmp(buf, expected, sizeof(expected)));
+}
+
+static void test_serialize_camera_zoom_out(void) {
+ struct sc_control_msg msg = {
+ .type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
+ };
+
+ uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
+ size_t size = sc_control_msg_serialize(&msg, buf);
+ assert(size == 1);
+
+ const uint8_t expected[] = {
+ SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
+ };
+ assert(!memcmp(buf, expected, sizeof(expected)));
+}
+
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@@ -490,5 +520,7 @@ int main(int argc, char *argv[]) {
test_serialize_start_app();
test_serialize_reset_video();
test_serialize_camera_set_torch();
+ test_serialize_camera_zoom_in();
+ test_serialize_camera_zoom_out();
return 0;
}
diff --git a/doc/shortcuts.md b/doc/shortcuts.md
index 04eca6f4..3a439d17 100644
--- a/doc/shortcuts.md
+++ b/doc/shortcuts.md
@@ -60,6 +60,8 @@ _[Super] is typically the Windows or Cmd key._
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
| Turn on the camera torch (camera mode only) | MOD+t
| Turn off the camera torch (camera mode only)| MOD+Shift+t
+ | Zoom camera in (camera mode only) | MOD+↑ _(up)_
+ | Zoom camera out (camera mode only) | MOD+↓ _(down)_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
index e98b7e81..6aea226e 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
@@ -26,6 +26,8 @@ public final class ControlMessage {
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final int TYPE_CAMERA_SET_TORCH = 18;
+ public static final int TYPE_CAMERA_ZOOM_IN = 19;
+ public static final int TYPE_CAMERA_ZOOM_OUT = 20;
public static final long SEQUENCE_INVALID = 0;
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
index 52b90181..fd29edbe 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
@@ -47,6 +47,8 @@ public class ControlMessageReader {
case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
+ case ControlMessage.TYPE_CAMERA_ZOOM_IN:
+ case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate();
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
index 9d8f3699..968663a0 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
@@ -376,6 +376,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_CAMERA_SET_TORCH:
cameraCapture.setTorchEnabled(msg.getOn());
return true;
+ case ControlMessage.TYPE_CAMERA_ZOOM_IN:
+ cameraCapture.zoomIn();
+ return true;
+ case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
+ cameraCapture.zoomOut();
+ return true;
default:
// fall through
}
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 2f872981..169edf51 100644
--- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java
@@ -54,6 +54,8 @@ public class CameraCapture extends SurfaceCapture {
0, 1, 0, 1, // column 4
};
+ private static final float ZOOM_FACTOR = 1 + 1 / 16f;
+
private final String explicitCameraId;
private final CameraFacing cameraFacing;
private final Size explicitSize;
@@ -472,6 +474,36 @@ public class CameraCapture extends SurfaceCapture {
});
}
+ private void zoom(boolean in) {
+ cameraHandler.post(() -> {
+ assertCameraThread();
+ if (currentSession != null && requestBuilder != null) {
+ // Always align to log values
+ double z = Math.round(Math.log(zoom) / Math.log(ZOOM_FACTOR));
+ double dir = in ? 1 : -1;
+ zoom = (float) Math.pow(ZOOM_FACTOR, z + dir);
+
+ try {
+ zoom = clampZoom(zoom);
+ Ln.i("Set camera zoom: " + zoom);
+ requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
+ CaptureRequest request = requestBuilder.build();
+ setRepeatingRequest(currentSession, request);
+ } catch (CameraAccessException e) {
+ Ln.e("Camera error", e);
+ }
+ }
+ });
+ }
+
+ public void zoomIn() {
+ zoom(true);
+ }
+
+ public void zoomOut() {
+ zoom(false);
+ }
+
private float clampZoom(float value) {
assertCameraThread();
if (zoomRange == null) {
diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
index 8fbe7c7f..4fcf66c9 100644
--- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
@@ -440,6 +440,38 @@ public class ControlMessageReaderTest {
Assert.assertEquals(-1, bis.read()); // EOS
}
+ @Test
+ public void testParseCameraZoomIn() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_IN);
+ byte[] packet = bos.toByteArray();
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(packet);
+ ControlMessageReader reader = new ControlMessageReader(bis);
+
+ ControlMessage event = reader.read();
+ Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_IN, event.getType());
+
+ Assert.assertEquals(-1, bis.read()); // EOS
+ }
+
+ @Test
+ public void testParseCameraZoomOut() throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_OUT);
+ byte[] packet = bos.toByteArray();
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(packet);
+ ControlMessageReader reader = new ControlMessageReader(bis);
+
+ ControlMessage event = reader.read();
+ Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_OUT, event.getType());
+
+ Assert.assertEquals(-1, bis.read()); // EOS
+ }
+
@Test
public void testMultiEvents() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();