diff --git a/app/meson.build b/app/meson.build index f7df69eb..936ab320 100644 --- a/app/meson.build +++ b/app/meson.build @@ -30,6 +30,7 @@ src = [ 'src/packet_merger.c', 'src/receiver.c', 'src/recorder.c', + 'src/screencap.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', @@ -117,6 +118,7 @@ dependencies = [ dependency('libavcodec', version: '>= 57.37', static: static), dependency('libavutil', static: static), dependency('libswresample', static: static), + dependency('libswscale', static: static), dependency('sdl2', version: '>= 2.0.5', static: static), ] diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..47fa344c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -114,6 +114,8 @@ enum { OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, OPT_DISPLAY_IME_POLICY, + OPT_SCREENCAP, + OPT_CONTROL_CMD, }; struct sc_option { @@ -612,6 +614,16 @@ static const struct sc_option options[] = { .longopt = "no-control", .text = "Disable device control (mirror the device in read-only).", }, + { + .longopt_id = OPT_CONTROL_CMD, + .longopt = "control", + .argdesc = "command", + .optional_arg = true, + .text = "Send a control command to the device and exit.\n" + "Use --control or --control --help for detailed usage.\n" + "Can be specified multiple times for multi-finger gestures.\n" + "Available commands: click, swipe, input.", + }, { .shortopt = 'N', .longopt = "no-playback", @@ -847,6 +859,14 @@ static const struct sc_option options[] = { .longopt = "rotation", .argdesc = "value", }, + { + .longopt_id = OPT_SCREENCAP, + .longopt = "screencap", + .argdesc = "file.png", + .text = "Take a screenshot and save it to file.\n" + "The screenshot is captured from the video stream, " + "the same way as --record.", + }, { .shortopt = 's', .longopt = "serial", @@ -1499,6 +1519,74 @@ print_exit_status(const struct sc_exit_status *status, unsigned cols) { free(text); } +static void +print_control_usage(void) { + fprintf(stderr, + "Usage: scrcpy --control=\"\" [--control=\"\" ...]\n" + "\n" + "Send control commands to the device and exit.\n" + "\n" + "Commands:\n" + "\n" + " click [duration_ms]\n" + " Tap at the given coordinates.\n" + " Default duration: 100ms. Use longer duration for long-press.\n" + "\n" + " Examples:\n" + " scrcpy --control=\"click 540 1200\"\n" + " scrcpy --control=\"click 540 1200 2000\" # long-press 2s\n" + "\n" + " swipe [duration_ms]\n" + " Swipe from (x1,y1) to (x2,y2).\n" + " Default duration: 300ms.\n" + "\n" + " Examples:\n" + " scrcpy --control=\"swipe 540 1500 540 500\"\n" + " scrcpy --control=\"swipe 540 1500 540 500 1000\" # slow\n" + "\n" + " input ''\n" + " Input text. Supports full Unicode (Chinese, emoji, etc).\n" + "\n" + " Examples:\n" + " scrcpy --control=\"input 'hello world'\"\n" + " scrcpy --control=\"input '你好世界🎉'\"\n" + "\n" + " sleep \n" + " Wait for the specified duration. Only used with &&.\n" + "\n" + "Chaining with &&:\n" + " Use && to chain commands sequentially within one --control.\n" + "\n" + " # Swipe up twice with 100ms gap\n" + " scrcpy --control=\"swipe 540 1500 540 500 300 && \\\n" + " sleep 100 && \\\n" + " swipe 540 1500 540 500 300\"\n" + "\n" + " # Type text then click search\n" + " scrcpy --control=\"input '你好' && click 900 200\"\n" + "\n" + " # Scroll down 5 times quickly\n" + " scrcpy --control=\"swipe 540 1500 540 500 200 && \\\n" + " swipe 540 1500 540 500 200 && \\\n" + " swipe 540 1500 540 500 200 && \\\n" + " swipe 540 1500 540 500 200 && \\\n" + " swipe 540 1500 540 500 200\"\n" + "\n" + "Multi-finger gestures:\n" + " Multiple --control flags (without &&) run in parallel.\n" + " Each flag represents one finger.\n" + "\n" + " # Two-finger pinch\n" + " scrcpy --control=\"swipe 200 800 400 500\" \\\n" + " --control=\"swipe 800 800 600 500\"\n" + "\n" + " # Three-finger swipe up\n" + " scrcpy --control=\"swipe 200 1200 200 400 500\" \\\n" + " --control=\"swipe 540 1200 540 400 500\" \\\n" + " --control=\"swipe 880 1200 880 400 500\"\n" + ); +} + void scrcpy_print_usage(const char *arg0) { #define SC_TERM_COLS_DEFAULT 80 @@ -2821,6 +2909,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_SCREENCAP: + opts->screencap_filename = optarg; + break; + case OPT_CONTROL_CMD: + if (!optarg || !strcmp(optarg, "-h") + || !strcmp(optarg, "--help") + || !strcmp(optarg, "help")) { + print_control_usage(); + return false; + } + if (opts->control_cmd_count >= SC_MAX_CONTROL_CMDS) { + LOGE("Too many --control commands (max %d)", + SC_MAX_CONTROL_CMDS); + return false; + } + opts->control_cmds[opts->control_cmd_count++] = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -2858,6 +2963,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], v4l2 = !!opts->v4l2_device; #endif + if (opts->control_cmd_count) { + // Control command mode: send commands and exit + opts->video_playback = false; + opts->audio_playback = false; + opts->audio = false; + opts->window = false; + opts->control = true; + } + if (!opts->window) { // Without window, there cannot be any video playback opts->video_playback = false; @@ -2876,8 +2990,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->video && !opts->video_playback && !opts->record_filename - && !v4l2) { - LOGI("No video playback, no recording, no V4L2 sink: video disabled"); + && !opts->screencap_filename && !v4l2) { + LOGI("No video playback, no recording, no screencap, no V4L2 sink: " + "video disabled"); opts->video = false; } @@ -3229,6 +3344,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->screencap_filename) { + if (!opts->video) { + LOGE("Video disabled, nothing to screencap"); + return false; + } + } + if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { LOGW("--audio-bit-rate is ignored for FLAC audio codec"); } @@ -3291,6 +3413,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("OTG mode: cannot record"); return false; } + if (opts->screencap_filename) { + LOGE("OTG mode: cannot screencap"); + return false; + } if (opts->turn_screen_off) { LOGE("OTG mode: could not turn screen off"); return false; diff --git a/app/src/events.h b/app/src/events.h index 2fe4d3a7..10a9acd4 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -20,6 +20,8 @@ enum { SC_EVENT_TIME_LIMIT_REACHED, SC_EVENT_CONTROLLER_ERROR, SC_EVENT_AOA_OPEN_ERROR, + SC_EVENT_SCREENCAP_COMPLETED, + SC_EVENT_SCREENCAP_ERROR, }; bool diff --git a/app/src/options.h b/app/src/options.h index 03b42913..9d4a6f06 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -227,6 +227,8 @@ struct sc_port_range { #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) +#define SC_MAX_CONTROL_CMDS 100 + struct scrcpy_options { const char *serial; const char *crop; @@ -327,6 +329,9 @@ struct scrcpy_options { const char *start_app; bool vd_destroy_content; bool vd_system_decorations; + const char *screencap_filename; + const char *control_cmds[SC_MAX_CONTROL_CMDS]; + unsigned control_cmd_count; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9c..fa555f33 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -24,6 +24,7 @@ #include "keyboard_sdk.h" #include "mouse_sdk.h" #include "recorder.h" +#include "screencap.h" #include "screen.h" #include "server.h" #include "uhid/gamepad_uhid.h" @@ -54,6 +55,7 @@ struct scrcpy { struct sc_decoder video_decoder; struct sc_decoder audio_decoder; struct sc_recorder recorder; + struct sc_screencap screencap; struct sc_delay_buffer video_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; @@ -192,6 +194,12 @@ event_loop(struct scrcpy *s, bool has_screen) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_SCREENCAP_COMPLETED: + LOGD("Screencap completed"); + return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_SCREENCAP_ERROR: + LOGE("Screencap error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_AOA_OPEN_ERROR: LOGE("AOA open error"); return SCRCPY_EXIT_FAILURE; @@ -270,6 +278,19 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, } } +static void +sc_screencap_on_ended(struct sc_screencap *screencap, bool success, + void *userdata) { + (void) screencap; + (void) userdata; + + if (success) { + sc_push_event(SC_EVENT_SCREENCAP_COMPLETED); + } else { + sc_push_event(SC_EVENT_SCREENCAP_ERROR); + } +} + static void sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, enum sc_demuxer_status status, void *userdata) { @@ -377,6 +398,526 @@ init_sdl_gamepads(void) { } } +struct sc_finger_action { + int x1, y1; // start + int x2, y2; // end (same as start for click) + int duration; // ms + bool is_swipe; +}; + +static bool +sc_parse_touch_cmd(const char *cmd_str, struct sc_finger_action *action) { + char *cmd = strdup(cmd_str); + if (!cmd) { + LOG_OOM(); + return false; + } + + char *saveptr; + char *token = strtok_r(cmd, " ", &saveptr); + if (!token) { + LOGE("Invalid control command (empty)"); + free(cmd); + return false; + } + + if (strcmp(token, "click") == 0) { + char *x_str = strtok_r(NULL, " ", &saveptr); + char *y_str = strtok_r(NULL, " ", &saveptr); + char *dur_str = strtok_r(NULL, " ", &saveptr); + + if (!x_str || !y_str) { + LOGE("Usage: click [duration_ms]"); + free(cmd); + return false; + } + + action->x1 = action->x2 = atoi(x_str); + action->y1 = action->y2 = atoi(y_str); + action->duration = dur_str ? atoi(dur_str) : 100; + action->is_swipe = false; + } else if (strcmp(token, "swipe") == 0) { + char *x1_str = strtok_r(NULL, " ", &saveptr); + char *y1_str = strtok_r(NULL, " ", &saveptr); + char *x2_str = strtok_r(NULL, " ", &saveptr); + char *y2_str = strtok_r(NULL, " ", &saveptr); + char *dur_str = strtok_r(NULL, " ", &saveptr); + + if (!x1_str || !y1_str || !x2_str || !y2_str) { + LOGE("Usage: swipe [duration_ms]"); + free(cmd); + return false; + } + + action->x1 = atoi(x1_str); + action->y1 = atoi(y1_str); + action->x2 = atoi(x2_str); + action->y2 = atoi(y2_str); + action->duration = dur_str ? atoi(dur_str) : 300; + action->is_swipe = true; + } else { + LOGE("Unknown touch command: %s", token); + free(cmd); + return false; + } + + free(cmd); + return true; +} + +static bool +sc_execute_touch_cmds(struct sc_controller *controller, + const char *const *cmds, unsigned count, + uint64_t base_pointer_id) { + struct sc_finger_action actions[SC_MAX_CONTROL_CMDS]; + + for (unsigned i = 0; i < count; i++) { + if (!sc_parse_touch_cmd(cmds[i], &actions[i])) { + return false; + } + } + + int max_duration = 0; + for (unsigned i = 0; i < count; i++) { + if (actions[i].duration > max_duration) { + max_duration = actions[i].duration; + } + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg.inject_touch_event.position.screen_size = + (struct sc_size){UINT16_MAX, UINT16_MAX}; + msg.inject_touch_event.action_button = 0; + msg.inject_touch_event.buttons = 0; + + // Send DOWN for all fingers + for (unsigned i = 0; i < count; i++) { + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_DOWN; + msg.inject_touch_event.pointer_id = base_pointer_id + i; + msg.inject_touch_event.position.point.x = actions[i].x1; + msg.inject_touch_event.position.point.y = actions[i].y1; + msg.inject_touch_event.pressure = 1.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch down for finger %u", i); + } + + if (i + 1 < count) { + SDL_Delay(5); + } + } + + bool active[SC_MAX_CONTROL_CMDS]; + for (unsigned i = 0; i < count; i++) { + active[i] = true; + } + + int step_ms = 10; + for (int t = step_ms; t <= max_duration + step_ms; t += step_ms) { + SDL_Delay(step_ms); + + for (unsigned i = 0; i < count; i++) { + if (!active[i]) { + continue; + } + + if (t >= actions[i].duration) { + // Send UP + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_UP; + msg.inject_touch_event.pointer_id = base_pointer_id + i; + msg.inject_touch_event.position.point.x = actions[i].x2; + msg.inject_touch_event.position.point.y = actions[i].y2; + msg.inject_touch_event.pressure = 0.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch up for finger %u", i); + } + + active[i] = false; + } else if (actions[i].is_swipe) { + // Interpolate position + float progress = (float)t / actions[i].duration; + int x = actions[i].x1 + + (int)((actions[i].x2 - actions[i].x1) * progress); + int y = actions[i].y1 + + (int)((actions[i].y2 - actions[i].y1) * progress); + + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + msg.inject_touch_event.pointer_id = base_pointer_id + i; + msg.inject_touch_event.position.point.x = x; + msg.inject_touch_event.position.point.y = y; + msg.inject_touch_event.pressure = 1.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch move for finger %u", i); + } + } + } + } + + return true; +} + +static bool +sc_execute_continuous_swipe(struct sc_controller *controller, + const struct sc_finger_action *segments, + unsigned count, uint64_t pointer_id) { + int total_duration = 0; + for (unsigned i = 0; i < count; i++) { + total_duration += segments[i].duration; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg.inject_touch_event.position.screen_size = + (struct sc_size){UINT16_MAX, UINT16_MAX}; + msg.inject_touch_event.action_button = 0; + msg.inject_touch_event.buttons = 0; + + // DOWN at start of first segment + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_DOWN; + msg.inject_touch_event.pointer_id = pointer_id; + msg.inject_touch_event.position.point.x = segments[0].x1; + msg.inject_touch_event.position.point.y = segments[0].y1; + msg.inject_touch_event.pressure = 1.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch down for continuous swipe"); + } + + int step_ms = 10; + for (int t = step_ms; t <= total_duration; t += step_ms) { + SDL_Delay(step_ms); + + // Find which segment we're in + int cumulative = 0; + unsigned seg = 0; + for (unsigned i = 0; i < count; i++) { + if (t <= cumulative + segments[i].duration) { + seg = i; + break; + } + cumulative += segments[i].duration; + } + + float progress = (float)(t - cumulative) / segments[seg].duration; + int x = segments[seg].x1 + + (int)((segments[seg].x2 - segments[seg].x1) * progress); + int y = segments[seg].y1 + + (int)((segments[seg].y2 - segments[seg].y1) * progress); + + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + msg.inject_touch_event.pointer_id = pointer_id; + msg.inject_touch_event.position.point.x = x; + msg.inject_touch_event.position.point.y = y; + msg.inject_touch_event.pressure = 1.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch move for continuous swipe"); + } + } + + // UP at end of last segment + msg.inject_touch_event.action = AMOTION_EVENT_ACTION_UP; + msg.inject_touch_event.pointer_id = pointer_id; + msg.inject_touch_event.position.point.x = segments[count - 1].x2; + msg.inject_touch_event.position.point.y = segments[count - 1].y2; + msg.inject_touch_event.pressure = 0.0f; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not send touch up for continuous swipe"); + } + + return true; +} + +static bool +sc_execute_input_cmd(struct sc_controller *controller, const char *cmd_str) { + // Skip "input " prefix + const char *text = cmd_str + 6; // strlen("input ") + + // Skip optional surrounding quotes + size_t len = strlen(text); + if (len >= 2 + && ((text[0] == '\'' && text[len - 1] == '\'') + || (text[0] == '"' && text[len - 1] == '"'))) { + // Strip quotes + char *unquoted = strndup(text + 1, len - 2); + if (!unquoted) { + LOG_OOM(); + return false; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.sequence = SC_SEQUENCE_INVALID; + msg.set_clipboard.text = unquoted; + msg.set_clipboard.paste = true; + + if (!sc_controller_push_msg(controller, &msg)) { + free(unquoted); + LOGE("Could not inject text"); + return false; + } + + LOGI("Text injected: %s", unquoted); + } else { + char *text_dup = strdup(text); + if (!text_dup) { + LOG_OOM(); + return false; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.sequence = SC_SEQUENCE_INVALID; + msg.set_clipboard.text = text_dup; + msg.set_clipboard.paste = true; + + if (!sc_controller_push_msg(controller, &msg)) { + free(text_dup); + LOGE("Could not inject text"); + return false; + } + + LOGI("Text injected: %s", text_dup); + } + + return true; +} + +static bool +sc_execute_single_step(struct sc_controller *controller, const char *cmd, + uint64_t pointer_id) { + // Trim leading whitespace + while (*cmd == ' ') { + cmd++; + } + + if (!*cmd) { + return true; // empty step, skip + } + + if (!strncmp(cmd, "sleep ", 6) || !strcmp(cmd, "sleep")) { + int ms = 0; + if (strlen(cmd) > 6) { + ms = atoi(cmd + 6); + } + if (ms > 0) { + SDL_Delay(ms); + } + return true; + } + + if (!strncmp(cmd, "input ", 6)) { + return sc_execute_input_cmd(controller, cmd); + } + + if (!strncmp(cmd, "click ", 6) || !strncmp(cmd, "swipe ", 6)) { + return sc_execute_touch_cmds(controller, &cmd, 1, pointer_id); + } + + LOGE("Unknown control command: %s", cmd); + return false; +} + +// Execute a single --control arg: if it contains "&&", split and run steps +// serially; consecutive connected swipes without sleep are merged into one +// continuous stroke. +static bool +sc_execute_one_control_arg(struct sc_controller *controller, const char *arg, + uint64_t pointer_id) { + if (!strstr(arg, "&&")) { + // No "&&": execute as a single command + return sc_execute_single_step(controller, arg, pointer_id); + } + + // Split by "&&" into steps array + char *dup = strdup(arg); + if (!dup) { + LOG_OOM(); + return false; + } + + char *steps[SC_MAX_CONTROL_CMDS]; + unsigned step_count = 0; + + char *remaining = dup; + while (remaining && *remaining && step_count < SC_MAX_CONTROL_CMDS) { + char *sep = strstr(remaining, "&&"); + if (sep) { + *sep = '\0'; + } + + // Trim whitespace + char *step = remaining; + while (*step == ' ') { + step++; + } + size_t slen = strlen(step); + while (slen > 0 && step[slen - 1] == ' ') { + step[--slen] = '\0'; + } + + if (*step) { + steps[step_count++] = step; + } + + if (sep) { + remaining = sep + 2; + } else { + break; + } + } + + bool ok = true; + unsigned i = 0; + + while (i < step_count && ok) { + // Check if this step is a swipe + struct sc_finger_action action; + if (!strncmp(steps[i], "swipe ", 6) + && sc_parse_touch_cmd(steps[i], &action) + && action.is_swipe) { + + // Look ahead for consecutive connected swipes + struct sc_finger_action segments[SC_MAX_CONTROL_CMDS]; + unsigned seg_count = 0; + segments[seg_count++] = action; + + unsigned j = i + 1; + while (j < step_count && seg_count < SC_MAX_CONTROL_CMDS) { + struct sc_finger_action next; + if (strncmp(steps[j], "swipe ", 6) != 0 + || !sc_parse_touch_cmd(steps[j], &next) + || !next.is_swipe) { + break; + } + // Check if connected: end of previous == start of next + if (segments[seg_count - 1].x2 != next.x1 + || segments[seg_count - 1].y2 != next.y1) { + break; + } + segments[seg_count++] = next; + j++; + } + + if (seg_count > 1) { + ok = sc_execute_continuous_swipe(controller, segments, + seg_count, pointer_id); + } else { + ok = sc_execute_single_step(controller, steps[i], pointer_id); + } + i = j; + } else { + ok = sc_execute_single_step(controller, steps[i], pointer_id); + i++; + } + } + + free(dup); + return ok; +} + +struct sc_control_thread_args { + struct sc_controller *controller; + const char *arg; + uint64_t pointer_id; + bool ok; +}; + +static int +sc_run_control_thread(void *data) { + struct sc_control_thread_args *args = data; + args->ok = sc_execute_one_control_arg(args->controller, args->arg, + args->pointer_id); + return 0; +} + +static bool +sc_execute_control_cmds(struct sc_controller *controller, + const struct scrcpy_options *options) { + unsigned count = options->control_cmd_count; + + if (count == 1) { + // Single --control arg: execute directly + return sc_execute_one_control_arg(controller, options->control_cmds[0], + 1); + } + + // Check if any --control arg contains "&&" + bool any_has_chain = false; + for (unsigned i = 0; i < count; i++) { + if (strstr(options->control_cmds[i], "&&")) { + any_has_chain = true; + break; + } + } + + if (!any_has_chain) { + // No "&&" anywhere: use the multi-touch parallel model + // (coordinated pointer_ids for simultaneous fingers) + const char *touch_cmds[SC_MAX_CONTROL_CMDS]; + unsigned touch_count = 0; + + for (unsigned i = 0; i < count; i++) { + const char *cmd = options->control_cmds[i]; + if (!strncmp(cmd, "input ", 6)) { + if (!sc_execute_input_cmd(controller, cmd)) { + return false; + } + } else if (!strncmp(cmd, "click ", 6) + || !strncmp(cmd, "swipe ", 6)) { + touch_cmds[touch_count++] = cmd; + } else { + LOGE("Unknown control command: %s", cmd); + return false; + } + } + + if (touch_count > 0) { + if (!sc_execute_touch_cmds(controller, touch_cmds, touch_count, + 1)) { + return false; + } + } + + return true; + } + + // At least one arg has "&&": each --control runs in its own thread + // (parallel between args, serial within each arg's "&&" chain). + struct sc_control_thread_args thread_args[SC_MAX_CONTROL_CMDS]; + sc_thread threads[SC_MAX_CONTROL_CMDS]; + + for (unsigned i = 0; i < count; i++) { + thread_args[i].controller = controller; + thread_args[i].arg = options->control_cmds[i]; + thread_args[i].pointer_id = (uint64_t)(i + 1); + thread_args[i].ok = false; + + if (!sc_thread_create(&threads[i], sc_run_control_thread, + "scrcpy-ctrl", &thread_args[i])) { + LOGE("Could not create control thread %u", i); + for (unsigned j = 0; j < i; j++) { + sc_thread_join(&threads[j], NULL); + } + return false; + } + } + + bool ok = true; + for (unsigned i = 0; i < count; i++) { + sc_thread_join(&threads[i], NULL); + if (!thread_args[i].ok) { + ok = false; + } + } + + return ok; +} + enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -400,6 +941,8 @@ scrcpy(struct scrcpy_options *options) { bool file_pusher_initialized = false; bool recorder_initialized = false; bool recorder_started = false; + bool screencap_initialized = false; + bool screencap_started = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif @@ -632,6 +1175,25 @@ scrcpy(struct scrcpy_options *options) { } } + if (options->screencap_filename) { + static const struct sc_screencap_callbacks screencap_cbs = { + .on_ended = sc_screencap_on_ended, + }; + if (!sc_screencap_init(&s->screencap, options->screencap_filename, + &screencap_cbs, NULL)) { + goto end; + } + screencap_initialized = true; + + if (!sc_screencap_start(&s->screencap)) { + goto end; + } + screencap_started = true; + + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->screencap.video_packet_sink); + } + struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; @@ -944,6 +1506,18 @@ aoa_complete: } } + if (options->control && options->control_cmd_count) { + assert(controller); + + bool ok = sc_execute_control_cmds(controller, options); + + // Wait for messages to be sent + SDL_Delay(100); + + ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; + goto end; + } + ret = event_loop(s, options->window); terminate_event_loop(); LOGD("quit..."); @@ -989,6 +1563,9 @@ end: if (recorder_initialized) { sc_recorder_stop(&s->recorder); } + if (screencap_initialized) { + sc_screencap_stop(&s->screencap); + } if (screen_initialized) { sc_screen_interrupt(&s->screen); } @@ -1053,6 +1630,13 @@ end: sc_recorder_destroy(&s->recorder); } + if (screencap_started) { + sc_screencap_join(&s->screencap); + } + if (screencap_initialized) { + sc_screencap_destroy(&s->screencap); + } + if (file_pusher_initialized) { sc_file_pusher_join(&s->file_pusher); sc_file_pusher_destroy(&s->file_pusher); diff --git a/app/src/screencap.c b/app/src/screencap.c new file mode 100644 index 00000000..e4f0d5c0 --- /dev/null +++ b/app/src/screencap.c @@ -0,0 +1,402 @@ +#include "screencap.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "util/log.h" + +/** Downcast packet sink to screencap */ +#define DOWNCAST_VIDEO(SINK) \ + container_of(SINK, struct sc_screencap, video_packet_sink) + +static bool +sc_screencap_save_frame_as_png(const char *filename, AVFrame *frame) { + bool success = false; + + const AVCodec *png_codec = avcodec_find_encoder(AV_CODEC_ID_PNG); + if (!png_codec) { + LOGE("PNG encoder not found"); + return false; + } + + AVCodecContext *png_ctx = avcodec_alloc_context3(png_codec); + if (!png_ctx) { + LOG_OOM(); + return false; + } + + png_ctx->width = frame->width; + png_ctx->height = frame->height; + png_ctx->pix_fmt = AV_PIX_FMT_RGB24; + png_ctx->time_base = (AVRational){1, 1}; + + int ret = avcodec_open2(png_ctx, png_codec, NULL); + if (ret < 0) { + LOGE("Could not open PNG encoder"); + goto free_png_ctx; + } + + // Convert frame to RGB24 if needed + AVFrame *rgb_frame = av_frame_alloc(); + if (!rgb_frame) { + LOG_OOM(); + goto free_png_ctx; + } + + rgb_frame->format = AV_PIX_FMT_RGB24; + rgb_frame->width = frame->width; + rgb_frame->height = frame->height; + + ret = av_frame_get_buffer(rgb_frame, 0); + if (ret < 0) { + LOGE("Could not allocate RGB frame buffer"); + goto free_rgb_frame; + } + + struct SwsContext *sws_ctx = sws_getContext( + frame->width, frame->height, frame->format, + rgb_frame->width, rgb_frame->height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, NULL, NULL, NULL); + if (!sws_ctx) { + LOGE("Could not create sws context"); + goto free_rgb_frame; + } + + sws_scale(sws_ctx, (const uint8_t * const *)frame->data, frame->linesize, + 0, frame->height, rgb_frame->data, rgb_frame->linesize); + sws_freeContext(sws_ctx); + + // Encode to PNG + AVPacket *pkt = av_packet_alloc(); + if (!pkt) { + LOG_OOM(); + goto free_rgb_frame; + } + + ret = avcodec_send_frame(png_ctx, rgb_frame); + if (ret < 0) { + LOGE("Could not send frame to PNG encoder"); + goto free_pkt; + } + + ret = avcodec_receive_packet(png_ctx, pkt); + if (ret < 0) { + LOGE("Could not receive PNG packet"); + goto free_pkt; + } + + // Write PNG data to file + FILE *fp = fopen(filename, "wb"); + if (!fp) { + LOGE("Could not open output file: %s", filename); + goto free_pkt; + } + + size_t written = fwrite(pkt->data, 1, pkt->size, fp); + fclose(fp); + + if (written != (size_t)pkt->size) { + LOGE("Failed to write PNG data to %s", filename); + goto free_pkt; + } + + success = true; + +free_pkt: + av_packet_free(&pkt); +free_rgb_frame: + av_frame_free(&rgb_frame); +free_png_ctx: + avcodec_free_context(&png_ctx); + + return success; +} + +static bool +sc_screencap_decode_and_save(struct sc_screencap *screencap) { + AVCodecContext *ctx = screencap->codec_ctx; + + // Set extradata from config packet if available + if (screencap->has_config && screencap->config_packet) { + uint8_t *extradata = av_malloc(screencap->config_packet->size + + AV_INPUT_BUFFER_PADDING_SIZE); + if (!extradata) { + LOG_OOM(); + return false; + } + memcpy(extradata, screencap->config_packet->data, + screencap->config_packet->size); + memset(extradata + screencap->config_packet->size, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + av_free(ctx->extradata); + ctx->extradata = extradata; + ctx->extradata_size = screencap->config_packet->size; + } + + int ret = avcodec_send_packet(ctx, screencap->video_packet); + if (ret < 0) { + LOGE("Screencap: could not send video packet to decoder: %d", ret); + return false; + } + + AVFrame *frame = av_frame_alloc(); + if (!frame) { + LOG_OOM(); + return false; + } + + ret = avcodec_receive_frame(ctx, frame); + if (ret < 0) { + LOGE("Screencap: could not receive decoded frame: %d", ret); + av_frame_free(&frame); + return false; + } + + bool ok = sc_screencap_save_frame_as_png(screencap->filename, frame); + av_frame_free(&frame); + + if (ok) { + LOGI("Screenshot saved to %s", screencap->filename); + } + + return ok; +} + +static int +run_screencap(void *data) { + struct sc_screencap *screencap = data; + + sc_mutex_lock(&screencap->mutex); + + while (!screencap->stopped && !screencap->video_packet_ready) { + sc_cond_wait(&screencap->cond, &screencap->mutex); + } + + if (screencap->stopped && !screencap->video_packet_ready) { + sc_mutex_unlock(&screencap->mutex); + screencap->cbs->on_ended(screencap, false, screencap->cbs_userdata); + return 0; + } + + sc_mutex_unlock(&screencap->mutex); + + bool success = sc_screencap_decode_and_save(screencap); + + // Signal that we are done - stop accepting packets + sc_mutex_lock(&screencap->mutex); + screencap->stopped = true; + screencap->captured = true; + sc_mutex_unlock(&screencap->mutex); + + screencap->cbs->on_ended(screencap, success, screencap->cbs_userdata); + + return 0; +} + +static bool +sc_screencap_video_packet_sink_open(struct sc_packet_sink *sink, + AVCodecContext *ctx) { + struct sc_screencap *screencap = DOWNCAST_VIDEO(sink); + + // Create our own decoder context using the same codec + const AVCodec *codec = ctx->codec; + AVCodecContext *dec_ctx = avcodec_alloc_context3(codec); + if (!dec_ctx) { + LOG_OOM(); + return false; + } + + // Copy parameters from the source context + dec_ctx->width = ctx->width; + dec_ctx->height = ctx->height; + dec_ctx->pix_fmt = ctx->pix_fmt; + dec_ctx->time_base = ctx->time_base; + + if (ctx->extradata_size > 0) { + dec_ctx->extradata = av_malloc(ctx->extradata_size + + AV_INPUT_BUFFER_PADDING_SIZE); + if (!dec_ctx->extradata) { + LOG_OOM(); + avcodec_free_context(&dec_ctx); + return false; + } + memcpy(dec_ctx->extradata, ctx->extradata, ctx->extradata_size); + memset(dec_ctx->extradata + ctx->extradata_size, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + dec_ctx->extradata_size = ctx->extradata_size; + } + + int ret = avcodec_open2(dec_ctx, codec, NULL); + if (ret < 0) { + LOGE("Screencap: could not open codec: %d", ret); + avcodec_free_context(&dec_ctx); + return false; + } + + screencap->codec_ctx = dec_ctx; + + return true; +} + +static void +sc_screencap_video_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_screencap *screencap = DOWNCAST_VIDEO(sink); + + sc_mutex_lock(&screencap->mutex); + screencap->stopped = true; + sc_cond_signal(&screencap->cond); + sc_mutex_unlock(&screencap->mutex); +} + +static bool +sc_screencap_video_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_screencap *screencap = DOWNCAST_VIDEO(sink); + + sc_mutex_lock(&screencap->mutex); + + if (screencap->stopped || screencap->video_packet_ready) { + // Already captured or stopped, reject further packets + sc_mutex_unlock(&screencap->mutex); + return false; + } + + bool is_config = packet->pts == AV_NOPTS_VALUE; + if (is_config) { + // Store the config packet (contains codec extradata like SPS/PPS) + if (screencap->config_packet) { + av_packet_free(&screencap->config_packet); + } + screencap->config_packet = av_packet_alloc(); + if (!screencap->config_packet) { + LOG_OOM(); + sc_mutex_unlock(&screencap->mutex); + return false; + } + if (av_packet_ref(screencap->config_packet, packet) < 0) { + av_packet_free(&screencap->config_packet); + sc_mutex_unlock(&screencap->mutex); + return false; + } + screencap->has_config = true; + sc_mutex_unlock(&screencap->mutex); + return true; + } + + // This is a real video packet (keyframe) - capture it + screencap->video_packet = av_packet_alloc(); + if (!screencap->video_packet) { + LOG_OOM(); + sc_mutex_unlock(&screencap->mutex); + return false; + } + if (av_packet_ref(screencap->video_packet, packet) < 0) { + av_packet_free(&screencap->video_packet); + sc_mutex_unlock(&screencap->mutex); + return false; + } + + screencap->video_packet_ready = true; + sc_cond_signal(&screencap->cond); + sc_mutex_unlock(&screencap->mutex); + + // Return false to detach from the demuxer pipeline after first frame + // This is intentional - we only need one frame + return false; +} + +bool +sc_screencap_init(struct sc_screencap *screencap, const char *filename, + const struct sc_screencap_callbacks *cbs, + void *cbs_userdata) { + screencap->filename = strdup(filename); + if (!screencap->filename) { + LOG_OOM(); + return false; + } + + bool ok = sc_mutex_init(&screencap->mutex); + if (!ok) { + goto error_free_filename; + } + + ok = sc_cond_init(&screencap->cond); + if (!ok) { + goto error_mutex_destroy; + } + + screencap->codec_ctx = NULL; + screencap->config_packet = NULL; + screencap->has_config = false; + screencap->captured = false; + screencap->stopped = false; + screencap->video_packet = NULL; + screencap->video_packet_ready = false; + + assert(cbs && cbs->on_ended); + screencap->cbs = cbs; + screencap->cbs_userdata = cbs_userdata; + + static const struct sc_packet_sink_ops video_ops = { + .open = sc_screencap_video_packet_sink_open, + .close = sc_screencap_video_packet_sink_close, + .push = sc_screencap_video_packet_sink_push, + }; + + screencap->video_packet_sink.ops = &video_ops; + + return true; + +error_mutex_destroy: + sc_mutex_destroy(&screencap->mutex); +error_free_filename: + free(screencap->filename); + + return false; +} + +bool +sc_screencap_start(struct sc_screencap *screencap) { + bool ok = sc_thread_create(&screencap->thread, run_screencap, + "scrcpy-screencap", screencap); + if (!ok) { + LOGE("Could not start screencap thread"); + return false; + } + + return true; +} + +void +sc_screencap_stop(struct sc_screencap *screencap) { + sc_mutex_lock(&screencap->mutex); + screencap->stopped = true; + sc_cond_signal(&screencap->cond); + sc_mutex_unlock(&screencap->mutex); +} + +void +sc_screencap_join(struct sc_screencap *screencap) { + sc_thread_join(&screencap->thread, NULL); +} + +void +sc_screencap_destroy(struct sc_screencap *screencap) { + if (screencap->codec_ctx) { + avcodec_free_context(&screencap->codec_ctx); + } + if (screencap->config_packet) { + av_packet_free(&screencap->config_packet); + } + if (screencap->video_packet) { + av_packet_free(&screencap->video_packet); + } + sc_cond_destroy(&screencap->cond); + sc_mutex_destroy(&screencap->mutex); + free(screencap->filename); +} diff --git a/app/src/screencap.h b/app/src/screencap.h new file mode 100644 index 00000000..a7160543 --- /dev/null +++ b/app/src/screencap.h @@ -0,0 +1,57 @@ +#ifndef SC_SCREENCAP_H +#define SC_SCREENCAP_H + +#include "common.h" + +#include +#include + +#include "trait/packet_sink.h" +#include "util/thread.h" + +struct sc_screencap { + struct sc_packet_sink video_packet_sink; + + char *filename; + + AVCodecContext *codec_ctx; + AVPacket *config_packet; // first config packet (extradata) + bool has_config; + bool captured; + + sc_mutex mutex; + sc_cond cond; + bool stopped; + + sc_thread thread; + + // The first non-config video packet to decode + AVPacket *video_packet; + bool video_packet_ready; + + const struct sc_screencap_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_screencap_callbacks { + void (*on_ended)(struct sc_screencap *screencap, bool success, + void *userdata); +}; + +bool +sc_screencap_init(struct sc_screencap *screencap, const char *filename, + const struct sc_screencap_callbacks *cbs, void *cbs_userdata); + +bool +sc_screencap_start(struct sc_screencap *screencap); + +void +sc_screencap_stop(struct sc_screencap *screencap); + +void +sc_screencap_join(struct sc_screencap *screencap); + +void +sc_screencap_destroy(struct sc_screencap *screencap); + +#endif diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index 8788021a..71fb769e 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -7,7 +7,7 @@ #include "trait/packet_sink.h" -#define SC_PACKET_SOURCE_MAX_SINKS 2 +#define SC_PACKET_SOURCE_MAX_SINKS 3 /** * Packet source trait