mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-12-17 05:24:19 +01:00
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:
@@ -819,6 +819,14 @@ Install APK from computer
|
|||||||
.B Drag & drop non-APK file
|
.B Drag & drop non-APK file
|
||||||
Push file to device (see \fB\-\-push\-target\fR)
|
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
|
.SH Environment variables
|
||||||
|
|
||||||
|
|||||||
@@ -1213,6 +1213,14 @@ static const struct sc_shortcut shortcuts[] = {
|
|||||||
.shortcuts = { "Drag & drop non-APK file" },
|
.shortcuts = { "Drag & drop non-APK file" },
|
||||||
.text = "Push file to device (see --push-target)",
|
.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[] = {
|
static const struct sc_envvar envvars[] = {
|
||||||
|
|||||||
@@ -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);
|
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
|
||||||
return 1 + len;
|
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_NOTIFICATION_PANEL:
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
||||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
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:
|
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||||
LOG_CMSG("reset video");
|
LOG_CMSG("reset video");
|
||||||
break;
|
break;
|
||||||
|
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
|
||||||
|
LOG_CMSG("camera set torch %s",
|
||||||
|
msg->camera_set_torch.on ? "on" : "off");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum sc_control_msg_type {
|
|||||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||||
SC_CONTROL_MSG_TYPE_START_APP,
|
SC_CONTROL_MSG_TYPE_START_APP,
|
||||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||||
|
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_copy_key {
|
enum sc_copy_key {
|
||||||
@@ -111,6 +112,9 @@ struct sc_control_msg {
|
|||||||
struct {
|
struct {
|
||||||
char *name;
|
char *name;
|
||||||
} start_app;
|
} start_app;
|
||||||
|
struct {
|
||||||
|
bool on;
|
||||||
|
} camera_set_torch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
static void
|
||||||
apply_orientation_transform(struct sc_input_manager *im,
|
apply_orientation_transform(struct sc_input_manager *im,
|
||||||
enum sc_orientation transform) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,6 +446,25 @@ static void test_serialize_reset_video(void) {
|
|||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
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[]) {
|
int main(int argc, char *argv[]) {
|
||||||
(void) argc;
|
(void) argc;
|
||||||
(void) argv;
|
(void) argv;
|
||||||
@@ -470,5 +489,6 @@ int main(int argc, char *argv[]) {
|
|||||||
test_serialize_open_hard_keyboard();
|
test_serialize_open_hard_keyboard();
|
||||||
test_serialize_start_app();
|
test_serialize_start_app();
|
||||||
test_serialize_reset_video();
|
test_serialize_reset_video();
|
||||||
|
test_serialize_camera_set_torch();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||||
public static final int TYPE_START_APP = 16;
|
public static final int TYPE_START_APP = 16;
|
||||||
public static final int TYPE_RESET_VIDEO = 17;
|
public static final int TYPE_RESET_VIDEO = 17;
|
||||||
|
public static final int TYPE_CAMERA_SET_TORCH = 18;
|
||||||
|
|
||||||
public static final long SEQUENCE_INVALID = 0;
|
public static final long SEQUENCE_INVALID = 0;
|
||||||
|
|
||||||
@@ -166,6 +167,13 @@ public final class ControlMessage {
|
|||||||
return msg;
|
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() {
|
public int getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ public class ControlMessageReader {
|
|||||||
return parseUhidDestroy();
|
return parseUhidDestroy();
|
||||||
case ControlMessage.TYPE_START_APP:
|
case ControlMessage.TYPE_START_APP:
|
||||||
return parseStartApp();
|
return parseStartApp();
|
||||||
|
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||||
|
return parseCameraSetTorch();
|
||||||
default:
|
default:
|
||||||
throw new ControlProtocolException("Unknown event type: " + type);
|
throw new ControlProtocolException("Unknown event type: " + type);
|
||||||
}
|
}
|
||||||
@@ -166,6 +168,11 @@ public class ControlMessageReader {
|
|||||||
return ControlMessage.createStartApp(name);
|
return ControlMessage.createStartApp(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ControlMessage parseCameraSetTorch() throws IOException {
|
||||||
|
boolean on = dis.readBoolean();
|
||||||
|
return ControlMessage.createCameraSetTorch(on);
|
||||||
|
}
|
||||||
|
|
||||||
private Position parsePosition() throws IOException {
|
private Position parsePosition() throws IOException {
|
||||||
int x = dis.readInt();
|
int x = dis.readInt();
|
||||||
int y = dis.readInt();
|
int y = dis.readInt();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.genymobile.scrcpy.device.Position;
|
|||||||
import com.genymobile.scrcpy.device.Size;
|
import com.genymobile.scrcpy.device.Size;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
import com.genymobile.scrcpy.util.LogUtils;
|
import com.genymobile.scrcpy.util.LogUtils;
|
||||||
|
import com.genymobile.scrcpy.video.CameraCapture;
|
||||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||||
import com.genymobile.scrcpy.video.VideoSource;
|
import com.genymobile.scrcpy.video.VideoSource;
|
||||||
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
||||||
@@ -99,7 +100,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
|
|
||||||
private boolean keepDisplayPowerOff;
|
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;
|
private SurfaceCapture surfaceCapture;
|
||||||
|
|
||||||
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
|
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
|
||||||
@@ -368,6 +369,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
default:
|
default:
|
||||||
// fall through
|
// 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);
|
throw new AssertionError("Unexpected message type: " + type);
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
|
|
||||||
private final AtomicBoolean disconnected = new AtomicBoolean();
|
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 boolean started;
|
||||||
|
private CaptureRequest.Builder requestBuilder;
|
||||||
|
private CameraCaptureSession currentSession;
|
||||||
|
|
||||||
public CameraCapture(Options options) {
|
public CameraCapture(Options options) {
|
||||||
this.explicitCameraId = options.getCameraId();
|
this.explicitCameraId = options.getCameraId();
|
||||||
@@ -287,7 +289,7 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||||
requestBuilder.addTarget(captureSurface);
|
requestBuilder.addTarget(captureSurface);
|
||||||
|
|
||||||
if (fps > 0) {
|
if (fps > 0) {
|
||||||
@@ -300,6 +302,7 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
|
|
||||||
CaptureRequest request = requestBuilder.build();
|
CaptureRequest request = requestBuilder.build();
|
||||||
setRepeatingRequest(session, request);
|
setRepeatingRequest(session, request);
|
||||||
|
currentSession = session;
|
||||||
} catch (CameraAccessException e) {
|
} catch (CameraAccessException e) {
|
||||||
Ln.e("Camera error", e);
|
Ln.e("Camera error", e);
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -325,6 +328,8 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
public void stop() {
|
public void stop() {
|
||||||
cameraHandler.post(() -> {
|
cameraHandler.post(() -> {
|
||||||
assertCameraThread();
|
assertCameraThread();
|
||||||
|
currentSession = null;
|
||||||
|
requestBuilder = null;
|
||||||
started = false;
|
started = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,6 +440,22 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
return disconnected.get();
|
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() {
|
private void assertCameraThread() {
|
||||||
assert Thread.currentThread() == cameraThread;
|
assert Thread.currentThread() == cameraThread;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,24 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(-1, bis.read()); // EOS
|
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
|
@Test
|
||||||
public void testMultiEvents() throws IOException {
|
public void testMultiEvents() throws IOException {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
|||||||
Reference in New Issue
Block a user