Add shortcuts to switch the camera torch

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

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Co-authored-by: Tommie <teh420@gmail.com>
This commit is contained in:
Romain Vimont 2025-11-28 22:32:05 +01:00
parent 553813e97d
commit 48fcfdd104
12 changed files with 140 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) {
@ -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);

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