From da72a3978f2bec293007264a4820ce208b53bb2e Mon Sep 17 00:00:00 2001 From: Tommie Date: Sun, 20 Jul 2025 11:50:16 -0400 Subject: [PATCH] Add camera torch and zoom --- app/data/bash-completion/scrcpy | 4 + app/data/zsh-completion/_scrcpy | 2 + app/meson.build | 1 + app/scrcpy.1 | 10 + app/src/cli.c | 36 +++- app/src/control_msg.c | 12 ++ app/src/control_msg.h | 3 + app/src/im/camera.c | 193 ++++++++++++++++++ app/src/im/camera.h | 11 + app/src/options.c | 2 + app/src/options.h | 2 + app/src/scrcpy.c | 3 + app/src/screen.c | 12 +- app/src/screen.h | 4 + app/src/server.c | 7 + app/src/server.h | 2 + doc/camera.md | 22 ++ doc/shortcuts.md | 3 + .../java/com/genymobile/scrcpy/Options.java | 16 ++ .../scrcpy/control/ControlMessage.java | 3 + .../scrcpy/control/ControlMessageReader.java | 3 + .../genymobile/scrcpy/control/Controller.java | 32 +++ .../scrcpy/video/CameraCapture.java | 79 ++++++- 23 files changed, 453 insertions(+), 9 deletions(-) create mode 100644 app/src/im/camera.c create mode 100644 app/src/im/camera.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a49da8ca..f8ae3dda 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -18,6 +18,8 @@ _scrcpy() { --camera-fps= --camera-high-speed --camera-size= + --camera-zoom-step= + --camera-torch --capture-orientation= --crop= -d --select-usb @@ -197,6 +199,8 @@ _scrcpy() { |--camera-id \ |--camera-fps \ |--camera-size \ + |--camera-zoom-step \ + |--camera-torch \ |--crop \ |--display-id \ |--max-fps \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 04ffb8f1..0a517794 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -25,6 +25,8 @@ arguments=( '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' + '--camera-zoom-step=[Specify the camera zoom step value]' + '--camera-torch[Turns the torch on when starting the camera]' '--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/meson.build b/app/meson.build index f7df69eb..25f11769 100644 --- a/app/meson.build +++ b/app/meson.build @@ -37,6 +37,7 @@ src = [ 'src/hid/hid_gamepad.c', 'src/hid/hid_keyboard.c', 'src/hid/hid_mouse.c', + 'src/im/camera.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/uhid/gamepad_uhid.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d72fda13..c6087435 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -131,6 +131,16 @@ The available camera ids can be listed by \fB\-\-list\-cameras\fR. .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. +.TP +.BI \-\-camera\-torch +Turns the torch on when starting the camera. + +.TP +.BI "\-\-camera\-zoom\-step " step +Specify the camera zoom step value. + +Default is 0.025. + .TP .BI "\-\-capture\-orientation " value Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..9bd0e679 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -93,6 +93,8 @@ enum { OPT_CAMERA_AR, OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, + OPT_CAMERA_ZOOM_STEP, + OPT_CAMERA_TORCH, OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, OPT_ORIENTATION, @@ -563,6 +565,17 @@ static const struct sc_option options[] = { .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, + { + .longopt_id = OPT_CAMERA_ZOOM_STEP, + .longopt = "camera-zoom-step", + .argdesc = "value", + .text = "Specify the camera zoom step value.", + }, + { + .longopt_id = OPT_CAMERA_TORCH, + .longopt = "camera-torch", + .text = "Turns the torch on when starting the camera.", + }, { .longopt_id = OPT_MOUSE, .longopt = "mouse", @@ -1142,6 +1155,18 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "Right-click (when screen is off)" }, .text = "Power on", }, + { + .shortcuts = { "MOD+q" }, + .text = "Camera toggle torch", + }, + { + .shortcuts = { "MOD+1" }, + .text = "Camera zoom in", + }, + { + .shortcuts = { "MOD+2" }, + .text = "Camera zoom out", + }, { .shortcuts = { "MOD+o" }, .text = "Turn device screen off (keep mirroring)", @@ -2420,6 +2445,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_MAX_FPS: opts->max_fps = optarg; break; + case OPT_CAMERA_ZOOM_STEP: + opts->camera_zoom_step = optarg; + break; + case OPT_CAMERA_TORCH: + opts->camera_torch = true; + break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { return false; @@ -3104,11 +3135,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("--camera-high-speed requires an explicit --camera-fps value"); return false; } - - if (opts->control) { - LOGI("Camera video source: control disabled"); - opts->control = false; - } } else if (opts->camera_id || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..532196a5 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -187,6 +187,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case SC_CONTROL_MSG_TYPE_CAMERA_TOGGLE_TORCH: + case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN: + case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT: case SC_CONTROL_MSG_TYPE_RESET_VIDEO: // no additional data return 1; @@ -318,6 +321,15 @@ 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_TOGGLE_TORCH: + LOG_CMSG("toggle camera torch"); + break; + case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN: + LOG_CMSG("camera zoom in"); + break; + case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT: + LOG_CMSG("camera zoom out"); + 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..a9b97abe 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -43,6 +43,9 @@ 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_TOGGLE_TORCH, + SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN, + SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT, }; enum sc_copy_key { diff --git a/app/src/im/camera.c b/app/src/im/camera.c new file mode 100644 index 00000000..6aaa10bd --- /dev/null +++ b/app/src/im/camera.c @@ -0,0 +1,193 @@ +#include "input_manager.h" + +#include +#include +#include +#include + +#include "android/input.h" +#include "android/keycodes.h" +#include "input_events.h" +#include "screen.h" +#include "shortcut_mod.h" +#include "util/log.h" +#include "camera.h" + +static void +camera_toggle_torch(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_CAMERA_TOGGLE_TORCH; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request camera toggle torch"); + } +} + +static void +camera_zoom_in(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request camera zoom in"); + } +} + +static void +camera_zoom_out(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request camera zoom out"); + } +} + +static void +reset_video(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request reset video"); + } +} + +static void +apply_orientation_transform(struct sc_input_manager *im, + enum sc_orientation transform) { + struct sc_screen *screen = im->screen; + enum sc_orientation new_orientation = + sc_orientation_apply(screen->orientation, transform); + sc_screen_set_orientation(screen, new_orientation); +} + +static void +sc_input_manager_process_key_camera(struct sc_input_manager *im, + const SDL_KeyboardEvent *event) { + bool control = im->controller; + bool paused = im->screen->paused; + bool video = im->screen->video; + + SDL_Keycode sdl_keycode = event->keysym.sym; + uint16_t mod = event->keysym.mod; + bool down = event->type == SDL_KEYDOWN; + bool shift = event->keysym.mod & KMOD_SHIFT; + bool repeat = event->repeat; + + uint16_t mods = im->sdl_shortcut_mods; + bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod) + || sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode); + + if (down && !repeat) { + if (sdl_keycode == im->last_keycode && mod == im->last_mod) { + ++im->key_repeat; + } else { + im->key_repeat = 0; + im->last_keycode = sdl_keycode; + im->last_mod = mod; + } + } + + if (is_shortcut) { + switch (sdl_keycode) { + // Camera + case SDLK_1: + if (control && video && !shift && down) { + camera_zoom_in(im); + } + return; + case SDLK_2: + if (control && video && !shift && down) { + camera_zoom_out(im); + } + return; + case SDLK_q: + if (control && video && !shift && !repeat && down) { + camera_toggle_torch(im); + } + return; + // Window + case SDLK_f: + if (video && !shift && !repeat && down) { + sc_screen_toggle_fullscreen(im->screen); + } + return; + case SDLK_w: + if (video && !shift && !repeat && down) { + sc_screen_resize_to_fit(im->screen); + } + return; + case SDLK_g: + if (video && !shift && !repeat && down) { + sc_screen_resize_to_pixel_perfect(im->screen); + } + return; + case SDLK_DOWN: + if (shift) { + if (video && !repeat && down) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_180); + } + } + return; + case SDLK_UP: + if (shift) { + if (video && !repeat && down) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_180); + } + } + return; + case SDLK_LEFT: + if (video && !repeat && down) { + if (shift) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im, + SC_ORIENTATION_270); + } + } + return; + case SDLK_RIGHT: + if (video && !repeat && down) { + if (shift) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im, + SC_ORIENTATION_90); + } + } + return; + // Video + case SDLK_r: + if (control && !repeat && down && !paused) { + if (shift) { + reset_video(im); + } + } + return; + } + } +} + +void +sc_input_manager_handle_event_camera(struct sc_input_manager *im, + const SDL_Event *event) { + switch (event->type) { + case SDL_KEYDOWN: + case SDL_KEYUP: + sc_input_manager_process_key_camera(im, &event->key); + break; + } +} diff --git a/app/src/im/camera.h b/app/src/im/camera.h new file mode 100644 index 00000000..54770a0e --- /dev/null +++ b/app/src/im/camera.h @@ -0,0 +1,11 @@ +#ifndef SC_CAMERA_H +#define SC_CAMERA_H + +#include "input_manager.h" + +#include + +void sc_input_manager_handle_event_camera(struct sc_input_manager *im, + const SDL_Event *event); + +#endif diff --git a/app/src/options.c b/app/src/options.c index 0fe82d29..a2b04bfc 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -52,6 +52,8 @@ const struct scrcpy_options scrcpy_options_default = { .video_bit_rate = 0, .audio_bit_rate = 0, .max_fps = NULL, + .camera_zoom_step = NULL, + .camera_torch = false, .capture_orientation = SC_ORIENTATION_0, .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, diff --git a/app/src/options.h b/app/src/options.h index 03b42913..dc354f42 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -262,6 +262,7 @@ struct scrcpy_options { uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server const char *angle; // float to be parsed by the server + const char *camera_zoom_step; // float to be parsed by the server enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; @@ -314,6 +315,7 @@ struct scrcpy_options { bool require_audio; bool kill_adb_on_close; bool camera_high_speed; + bool camera_torch; #define SC_OPTION_LIST_ENCODERS 0x1 #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a4c8c340..0510c815 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -471,6 +471,8 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, + .camera_zoom_step = options->camera_zoom_step, + .camera_torch = options->camera_torch, .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, .list = options->list, @@ -804,6 +806,7 @@ aoa_complete: struct sc_screen_params screen_params = { .video = options->video_playback, + .video_source = options->video_source, .controller = controller, .fp = fp, .kp = kp, diff --git a/app/src/screen.c b/app/src/screen.c index 1d694f12..9ea65865 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -8,6 +8,7 @@ #include "icon.h" #include "options.h" #include "util/log.h" +#include "im/camera.h" #define DISPLAY_MARGINS 96 @@ -335,6 +336,7 @@ sc_screen_init(struct sc_screen *screen, screen->orientation = SC_ORIENTATION_0; screen->video = params->video; + screen->video_source = params->video_source; screen->req.x = params->window_x; screen->req.y = params->window_y; @@ -867,7 +869,15 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } - sc_input_manager_handle_event(&screen->im, event); + switch (screen->video_source) { + case SC_VIDEO_SOURCE_DISPLAY: + sc_input_manager_handle_event(&screen->im, event); + break; + case SC_VIDEO_SOURCE_CAMERA: + sc_input_manager_handle_event_camera(&screen->im, event); + break; + } + return true; } diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2..4d018ae2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -69,6 +69,8 @@ struct sc_screen { bool paused; AVFrame *resume_frame; + + enum sc_video_source video_source; }; struct sc_screen_params { @@ -100,6 +102,8 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; + + enum sc_video_source video_source; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/app/src/server.c b/app/src/server.c index 153219c3..00652a1f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -354,6 +354,13 @@ execute_server(struct sc_server *server, if (params->camera_fps) { ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); } + if (params->camera_zoom_step) { + VALIDATE_STRING(params->camera_zoom_step); + ADD_PARAM("camera_zoom_step=%s", params->camera_zoom_step); + } + if (params->camera_torch) { + ADD_PARAM("camera_torch=true"); + } if (params->camera_high_speed) { ADD_PARAM("camera_high_speed=true"); } diff --git a/app/src/server.h b/app/src/server.h index 5f4592de..e4f2f3e6 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,8 @@ struct sc_server_params { uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server const char *angle; // float to be parsed by the server + const char *camera_zoom_step; // float to be parsed by the server + bool camera_torch; sc_tick screen_off_timeout; enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; diff --git a/doc/camera.md b/doc/camera.md index 32417694..80003ac3 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -147,6 +147,28 @@ scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240 [high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession +## Torch + +- MOD+q to toggle the torch. + +To turn the torch on when starting the camera, add `--camera-torch`. + +```bash +scrcpy --video-source=camera --camera-torch +``` + +## Zoom + +- MOD+1 to zoom in. +- MOD+2 to zoom out. + +To change the camera zoom step, set `--camera-zoom-step`. + +```bash +scrcpy --video-source=camera --camera-zoom-step=0.025 +``` + + ## Brace expansion tip All camera options start with `--camera-`, so if your shell supports it, you can diff --git a/doc/shortcuts.md b/doc/shortcuts.md index d22eb473..4a7533f9 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -53,6 +53,9 @@ _[Super] is typically the Windows or Cmd key._ | Inject computer clipboard text | MOD+Shift+v | Open keyboard settings (HID keyboard only) | MOD+k | Enable/disable FPS counter (on stdout) | MOD+i + | Camera torch toggle | MOD+q + | Camera zoom in | MOD+1 + | Camera zoom out | MOD+2 | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ | Tilt vertically (slide with 2 fingers) | Shift+_click-and-move_ | Tilt horizontally (slide with 2 fingers) | Ctrl+Shift+_click-and-move_ diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..4cfc6024 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -46,6 +46,8 @@ public class Options { private CameraAspectRatio cameraAspectRatio; private int cameraFps; private boolean cameraHighSpeed; + private float cameraZoomStep; + private boolean cameraTorch; private boolean showTouches; private boolean stayAwake; private int screenOffTimeout = -1; @@ -176,6 +178,14 @@ public class Options { return cameraHighSpeed; } + public float getCameraZoomStep() { + return cameraZoomStep; + } + + public boolean getCameraTorch() { + return cameraTorch; + } + public boolean getShowTouches() { return showTouches; } @@ -474,6 +484,12 @@ public class Options { case "camera_high_speed": options.cameraHighSpeed = Boolean.parseBoolean(value); break; + case "camera_zoom_step": + options.cameraZoomStep = Float.parseFloat(value); + break; + case "camera_torch": + options.cameraTorch = Boolean.parseBoolean(value); + break; case "new_display": options.newDisplay = parseNewDisplay(value); break; 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..cc869807 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,9 @@ 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_TOGGLE_TORCH = 18; + public static final int TYPE_CAMERA_ZOOM_IN = 19; + public static final int TYPE_CAMERA_ZOOM_OUT = 20; public static final long SEQUENCE_INVALID = 0; 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..039259ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -46,6 +46,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case ControlMessage.TYPE_CAMERA_TOGGLE_TORCH: + case ControlMessage.TYPE_CAMERA_ZOOM_IN: + case ControlMessage.TYPE_CAMERA_ZOOM_OUT: case ControlMessage.TYPE_RESET_VIDEO: return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: 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 b4a8e3ca..6571264d 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.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; @@ -99,6 +100,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Used for resetting video encoding on RESET_VIDEO message private SurfaceCapture surfaceCapture; + private CameraCapture cameraCapture; public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { this.displayId = options.getDisplayId(); @@ -150,6 +152,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { public void setSurfaceCapture(SurfaceCapture surfaceCapture) { this.surfaceCapture = surfaceCapture; + if (this.surfaceCapture instanceof CameraCapture) { + this.cameraCapture = (CameraCapture) this.surfaceCapture; + } } private UhidManager getUhidManager() { @@ -331,6 +336,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_RESET_VIDEO: resetVideo(); break; + case ControlMessage.TYPE_CAMERA_TOGGLE_TORCH: + cameraToggleTorch(); + break; + case ControlMessage.TYPE_CAMERA_ZOOM_IN: + cameraZoomIn(); + break; + case ControlMessage.TYPE_CAMERA_ZOOM_OUT: + cameraZoomOut(); + break; default: // do nothing } @@ -754,4 +768,22 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { surfaceCapture.requestInvalidate(); } } + + private void cameraToggleTorch() { + if (cameraCapture != null) { + cameraCapture.toggleTorch(); + } + } + + private void cameraZoomIn() { + if (cameraCapture != null) { + cameraCapture.zoomIn(); + } + } + + private void cameraZoomOut() { + if (cameraCapture != null) { + cameraCapture.zoomOut(); + } + } } 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 0e147cb7..d1f11122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -75,6 +75,13 @@ public class CameraCapture extends SurfaceCapture { private Handler cameraHandler; private CameraDevice cameraDevice; private Executor cameraExecutor; + private CameraCaptureSession session; + private CaptureRequest.Builder requestBuilder; + private boolean torchAvailable = false; + private boolean torchOn = false; + private float zoomLevel = 1f; + private float zoomStep = 0.025f; + private Range zoomRange = new Range<>(1f, 1f); private final AtomicBoolean disconnected = new AtomicBoolean(); @@ -90,6 +97,10 @@ public class CameraCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; this.angle = options.getAngle(); + if (options.getCameraZoomStep() > 0f) { + this.zoomStep = options.getCameraZoomStep(); + } + this.torchOn = options.getCameraTorch(); } @Override @@ -106,6 +117,19 @@ public class CameraCapture extends SurfaceCapture { } Ln.i("Using camera '" + cameraId + "'"); + + CameraCharacteristics characteristics = ServiceManager.getCameraManager().getCameraCharacteristics(cameraId); + + torchAvailable = Boolean.TRUE.equals(secureGet(characteristics, CameraCharacteristics.FLASH_INFO_AVAILABLE)); + if (!torchAvailable && torchOn) { + Ln.w("Camera flash unit not found."); + } + + zoomRange = secureGet(characteristics, CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + if (zoomRange != null) { + Ln.d(String.format("Camera zoom range: [%f, %f]", zoomRange.getLower(), zoomRange.getUpper())); + } + cameraDevice = openCamera(cameraId); } catch (CameraAccessException | InterruptedException e) { throw new IOException(e); @@ -262,7 +286,7 @@ public class CameraCapture extends SurfaceCapture { } try { - CameraCaptureSession session = createCaptureSession(cameraDevice, surface); + session = createCaptureSession(cameraDevice, surface); CaptureRequest request = createCaptureRequest(surface); setRepeatingRequest(session, request); } catch (CameraAccessException | InterruptedException e) { @@ -381,13 +405,17 @@ public class CameraCapture extends SurfaceCapture { } private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { - CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(surface); if (fps > 0) { requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps)); } + if (torchOn && torchAvailable) { + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + } + return requestBuilder.build(); } @@ -421,6 +449,51 @@ public class CameraCapture extends SurfaceCapture { @Override public void requestInvalidate() { - // do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring) + invalidate(); + } + + private static T secureGet(CameraCharacteristics characteristics, CameraCharacteristics.Key key) { + try { + return characteristics.get(key); + } catch (IllegalArgumentException e) { + Ln.w("Failed to get characteristic for key: " + key.getName(), e); + return null; + } + } + + public void toggleTorch() { + if (torchAvailable) { + torchOn = !torchOn; + try { + requestBuilder.set(CaptureRequest.FLASH_MODE, + (torchOn) ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF); + setRepeatingRequest(session, requestBuilder.build()); + } catch (CameraAccessException | InterruptedException e) { + Ln.e("Camera toggle torch error: ", e); + } + } + } + + private void setZoom(float level) { + if (zoomRange == null) { + return; + } + + try { + float zoomClamped = Math.max(zoomRange.getLower(), Math.min(level, zoomRange.getUpper())); + requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomClamped); + setRepeatingRequest(session, requestBuilder.build()); + zoomLevel = zoomClamped; + } catch (CameraAccessException | InterruptedException e) { + Ln.e("Camera set zoom error: ", e); + } + } + + public void zoomIn() { + setZoom(zoomLevel += zoomStep); + } + + public void zoomOut() { + setZoom(zoomLevel -= zoomStep); } }