Add shortcuts to switch the camera torch

MOD+l turns on the camera torch, MOD+Shift+l turns it off.

TODO ref 6243

Co-authored-by: Tommie <teh420@gmail.com>
This commit is contained in:
Romain Vimont
2025-11-28 22:32:05 +01:00
parent b07ce622e9
commit d9b364f114
11 changed files with 133 additions and 3 deletions

View File

@@ -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

View File

@@ -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[] = {

View File

@@ -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:

View File

@@ -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;
};
};

View File

@@ -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) {
@@ -572,6 +585,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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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) {
@@ -365,6 +366,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
default:
// fall through
}
} else {
switch (type) {
case ControlMessage.TYPE_CAMERA_SET_TORCH:
assert surfaceCapture instanceof CameraCapture;
CameraCapture cameraCapture = (CameraCapture) surfaceCapture;
cameraCapture.setTorchEnabled(msg.getOn());
return true;
default:
// fall through
}
}
throw new AssertionError("Unexpected message type: " + type);

View File

@@ -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) {
@@ -299,6 +301,7 @@ public class CameraCapture extends SurfaceCapture {
CaptureRequest request = requestBuilder.build();
setRepeatingRequest(session, request);
currentSession = session;
} catch (CameraAccessException e) {
Ln.e("Camera error", e);
invalidate();
@@ -324,6 +327,8 @@ public class CameraCapture extends SurfaceCapture {
public void stop() {
cameraHandler.post(() -> {
assertCameraThread();
currentSession = null;
requestBuilder = null;
started = false;
});
@@ -434,6 +439,21 @@ public class CameraCapture extends SurfaceCapture {
return disconnected.get();
}
public void setTorchEnabled(boolean enabled) {
cameraHandler.post(() -> {
assertCameraThread();
if (currentSession != null && requestBuilder != null) {
try {
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;
}

View File

@@ -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();