diff --git a/app/scrcpy.1 b/app/scrcpy.1
index 092eba36..d3366ae9 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+t
+Turn on the camera torch (camera mode only)
+
+.TP
+.B MOD+Shift+t
+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..3d5ae757 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+t" },
+ .text = "Turn on the camera torch (camera mode only)",
+ },
+ {
+ .shortcuts = { "MOD+Shift+t" },
+ .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 67540454..874843cc 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_T:
+ 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/doc/shortcuts.md b/doc/shortcuts.md
index d22eb473..04eca6f4 100644
--- a/doc/shortcuts.md
+++ b/doc/shortcuts.md
@@ -58,6 +58,8 @@ _[Super] is typically the Windows or Cmd key._
| Tilt horizontally (slide with 2 fingers) | Ctrl+Shift+_click-and-move_
| Drag & drop APK file | Install APK from computer
| 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
_¹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 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();