This commit is contained in:
yihuan zhao 2026-04-06 09:36:14 -04:00 committed by GitHub
commit bfb0a7951a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1181 additions and 3 deletions

View File

@ -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),
]

View File

@ -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=\"<command>\" [--control=\"<command>\" ...]\n"
"\n"
"Send control commands to the device and exit.\n"
"\n"
"Commands:\n"
"\n"
" click <x> <y> [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 <x1> <y1> <x2> <y2> [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 '<text>'\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 <ms>\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;

View File

@ -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

View File

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

View File

@ -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 <x> <y> [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 <x1> <y1> <x2> <y2> [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);

402
app/src/screencap.c Normal file
View File

@ -0,0 +1,402 @@
#include "screencap.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#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);
}

57
app/src/screencap.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef SC_SCREENCAP_H
#define SC_SCREENCAP_H
#include "common.h"
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#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

View File

@ -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