mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-04-14 15:21:51 +08:00
Merge 4485bde4ef into 247a37d57b
This commit is contained in:
commit
bfb0a7951a
@ -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),
|
||||
]
|
||||
|
||||
|
||||
130
app/src/cli.c
130
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=\"<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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
584
app/src/scrcpy.c
584
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 <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
402
app/src/screencap.c
Normal 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
57
app/src/screencap.h
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user