From f2420e59533c20994c167f68fcc5a2c14c7ba83e Mon Sep 17 00:00:00 2001 From: Tommie Date: Sun, 20 Jul 2025 11:50:16 -0400 Subject: [PATCH] Add shortcuts to change the camera zoom MOD+up and MOD+zoom change the camera zoom. TODO ref 6243 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 8 +++++ app/src/control_msg.h | 2 ++ app/src/input_manager.c | 36 +++++++++++++++++++ app/tests/test_control_msg_serialize.c | 32 +++++++++++++++++ .../scrcpy/control/ControlMessage.java | 2 ++ .../scrcpy/control/ControlMessageReader.java | 2 ++ .../genymobile/scrcpy/control/Controller.java | 6 ++++ .../scrcpy/video/CameraCapture.java | 32 +++++++++++++++++ .../control/ControlMessageReaderTest.java | 32 +++++++++++++++++ 9 files changed, 152 insertions(+) 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 4578815b..7f4ff696 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/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..7bbd8490 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 testParseCameraZoomIn() 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();