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

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