diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 092eba36..a2045132 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -819,6 +819,14 @@ Install APK from computer .B Drag & drop non-APK file Push file to device (see \fB\-\-push\-target\fR) +.TP +.B MOD+l +Turn on the camera torch (camera mode only) + +.TP +.B MOD+Shift+l +Turn off the camera torch (camera mode only) + .SH Environment variables diff --git a/app/src/cli.c b/app/src/cli.c index c79bb074..1549bd3e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1213,6 +1213,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "Drag & drop non-APK file" }, .text = "Push file to device (see --push-target)", }, + { + .shortcuts = { "MOD+l" }, + .text = "Turn on the camera torch (camera mode only)", + }, + { + .shortcuts = { "MOD+Shift+l" }, + .text = "Turn off the camera torch (camera mode only)", + }, }; static const struct sc_envvar envvars[] = { diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..8d74ea84 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -182,6 +182,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255); return 1 + len; } + case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH: + buf[1] = msg->camera_set_torch.on ? 1 : 0; + return 2; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -318,6 +321,10 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_RESET_VIDEO: LOG_CMSG("reset video"); break; + case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH: + LOG_CMSG("camera set torch %s", + msg->camera_set_torch.on ? "on" : "off"); + 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 74dbcba8..e1e9c5dc 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -43,6 +43,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_START_APP, SC_CONTROL_MSG_TYPE_RESET_VIDEO, + SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH, }; enum sc_copy_key { @@ -111,6 +112,9 @@ struct sc_control_msg { struct { char *name; } start_app; + struct { + bool on; + } camera_set_torch; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 00ad12df..4578815b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -302,6 +302,19 @@ reset_video(struct sc_input_manager *im) { } } +static void +camera_set_torch(struct sc_input_manager *im, bool on) { + assert(im->controller && im->camera); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH; + msg.camera_set_torch.on = on; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request setting camera torch"); + } +} + static void apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { @@ -581,6 +594,16 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + if (control && im->camera) { + switch (sdl_keycode) { + case SDLK_L: + if (!repeat && down) { + camera_set_torch(im, !shift); + } + return; + } + } + return; } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index e43c7ce4..c71a4570 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -446,6 +446,25 @@ static void test_serialize_reset_video(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_camera_set_torch(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH, + .camera_set_torch = { + .on = true, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 2); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH, + 0x01, // true + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -470,5 +489,6 @@ int main(int argc, char *argv[]) { test_serialize_open_hard_keyboard(); test_serialize_start_app(); test_serialize_reset_video(); + test_serialize_camera_set_torch(); 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 0eb96adc..e98b7e81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -25,6 +25,7 @@ public final class ControlMessage { public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; 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 long SEQUENCE_INVALID = 0; @@ -166,6 +167,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createCameraSetTorch(boolean on) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_CAMERA_SET_TORCH; + msg.on = on; + return msg; + } + public int getType() { return type; } 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 830a7ec7..52b90181 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -56,6 +56,8 @@ public class ControlMessageReader { return parseUhidDestroy(); case ControlMessage.TYPE_START_APP: return parseStartApp(); + case ControlMessage.TYPE_CAMERA_SET_TORCH: + return parseCameraSetTorch(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -166,6 +168,11 @@ public class ControlMessageReader { return ControlMessage.createStartApp(name); } + private ControlMessage parseCameraSetTorch() throws IOException { + boolean on = dis.readBoolean(); + return ControlMessage.createCameraSetTorch(on); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); 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 9ef13d09..9d8f3699 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.CameraCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VirtualDisplayListener; @@ -99,7 +100,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean keepDisplayPowerOff; - // Used for resetting video encoding on RESET_VIDEO message + // Used for resetting video encoding on RESET_VIDEO message or for sending camera controls private SurfaceCapture surfaceCapture; public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { @@ -368,6 +369,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { default: // fall through } + } else { + assert surfaceCapture instanceof CameraCapture; + CameraCapture cameraCapture = (CameraCapture) surfaceCapture; + switch (type) { + case ControlMessage.TYPE_CAMERA_SET_TORCH: + cameraCapture.setTorchEnabled(msg.getOn()); + return true; + default: + // fall through + } } throw new AssertionError("Unexpected message type: " + type); 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 a33fb9ad..143da87f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -80,8 +80,10 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - // Must be accessed only from the camera thread + // The following fields must be accessed only from the camera thread private boolean started; + private CaptureRequest.Builder requestBuilder; + private CameraCaptureSession currentSession; public CameraCapture(Options options) { this.explicitCameraId = options.getCameraId(); @@ -287,7 +289,7 @@ public class CameraCapture extends SurfaceCapture { } try { - CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(captureSurface); if (fps > 0) { @@ -300,6 +302,7 @@ public class CameraCapture extends SurfaceCapture { CaptureRequest request = requestBuilder.build(); setRepeatingRequest(session, request); + currentSession = session; } catch (CameraAccessException e) { Ln.e("Camera error", e); invalidate(); @@ -325,6 +328,8 @@ public class CameraCapture extends SurfaceCapture { public void stop() { cameraHandler.post(() -> { assertCameraThread(); + currentSession = null; + requestBuilder = null; started = false; }); @@ -435,6 +440,22 @@ public class CameraCapture extends SurfaceCapture { return disconnected.get(); } + public void setTorchEnabled(boolean enabled) { + cameraHandler.post(() -> { + assertCameraThread(); + if (currentSession != null && requestBuilder != null) { + try { + Ln.i("Turn camera torch " + (enabled ? "on" : "off")); + requestBuilder.set(CaptureRequest.FLASH_MODE, enabled ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF); + CaptureRequest request = requestBuilder.build(); + setRepeatingRequest(currentSession, request); + } catch (CameraAccessException e) { + Ln.e("Camera error", e); + } + } + }); + } + private void assertCameraThread() { assert Thread.currentThread() == cameraThread; } 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 0cc0a6b5..8fbe7c7f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -422,6 +422,24 @@ public class ControlMessageReaderTest { Assert.assertEquals(-1, bis.read()); // EOS } + @Test + public void testParseCameraSetTorch() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_CAMERA_SET_TORCH); + dos.writeBoolean(true); + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_CAMERA_SET_TORCH, event.getType()); + Assert.assertTrue(event.getOn()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + @Test public void testMultiEvents() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream();