mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-04-21 01:33:36 +00:00
Merge 8b627024b7 into 4671927c34
This commit is contained in:
commit
a94a6142d4
14 changed files with 581 additions and 117 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,6 +5,8 @@ build/
|
|||
/release-*/
|
||||
.idea/
|
||||
.gradle/
|
||||
.cache/
|
||||
.vscode/
|
||||
/x/
|
||||
local.properties
|
||||
/scrcpy-server
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ _scrcpy() {
|
|||
--screen-off-timeout=
|
||||
--shortcut-mod=
|
||||
--start-app=
|
||||
--exit-on-close
|
||||
-t --show-touches
|
||||
--tcpip
|
||||
--tcpip=
|
||||
|
|
|
|||
10
app/scrcpy.1
10
app/scrcpy.1
|
|
@ -536,9 +536,15 @@ Add a '+' prefix to force-stop before starting the app:
|
|||
|
||||
scrcpy --new-display --start-app=+org.mozilla.firefox
|
||||
|
||||
Both prefixes can be used, in that order:
|
||||
All prefixes can be combined, in that order.
|
||||
|
||||
scrcpy --start-app=+?firefox
|
||||
See also \fB\-\-exit\-on\-close\fR.
|
||||
|
||||
.TP
|
||||
.B \-\-exit\-on\-close
|
||||
Exit scrcpy when the app started with \fB\-\-start\-app\fR closes.
|
||||
|
||||
This option is only available with \fB\-\-start\-app\fR.
|
||||
|
||||
.TP
|
||||
.B \-t, \-\-show\-touches
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "options.h"
|
||||
#include "util/log.h"
|
||||
|
|
@ -108,6 +111,7 @@ enum {
|
|||
OPT_NEW_DISPLAY,
|
||||
OPT_LIST_APPS,
|
||||
OPT_START_APP,
|
||||
OPT_EXIT_ON_CLOSE,
|
||||
OPT_SCREEN_OFF_TIMEOUT,
|
||||
OPT_CAPTURE_ORIENTATION,
|
||||
OPT_ANGLE,
|
||||
|
|
@ -890,8 +894,14 @@ static const struct sc_option options[] = {
|
|||
" scrcpy --start-app=?firefox\n"
|
||||
"Add a '+' prefix to force-stop before starting the app:\n"
|
||||
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
|
||||
"Both prefixes can be used, in that order:\n"
|
||||
" scrcpy --start-app=+?firefox",
|
||||
"All prefixes can be combined, in that order.\n"
|
||||
"See also --exit-on-close.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_EXIT_ON_CLOSE,
|
||||
.longopt = "exit-on-close",
|
||||
.text = "Exit scrcpy when the app started with --start-app closes.\n"
|
||||
"This option is only available with --start-app.",
|
||||
},
|
||||
{
|
||||
.shortopt = 't',
|
||||
|
|
@ -2799,6 +2809,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||
break;
|
||||
case OPT_START_APP:
|
||||
opts->start_app = optarg;
|
||||
// Check if the app name ends with '-' to enable end execution
|
||||
if (optarg && strlen(optarg) > 0 && optarg[strlen(optarg) - 1] == '-') {
|
||||
opts->stop_app = true;
|
||||
// Remove the trailing '-' from the app name
|
||||
char *app_name = strdup(optarg);
|
||||
if (app_name) {
|
||||
app_name[strlen(app_name) - 1] = '\0';
|
||||
opts->start_app = app_name;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OPT_EXIT_ON_CLOSE:
|
||||
if (!opts->start_app) {
|
||||
LOGE("--exit-on-close is only available with --start-app");
|
||||
return false;
|
||||
}
|
||||
opts->exit_on_app_close = true;
|
||||
break;
|
||||
case OPT_SCREEN_OFF_TIMEOUT:
|
||||
if (!parse_screen_off_timeout(optarg,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||
.audio_dup = false,
|
||||
.new_display = NULL,
|
||||
.start_app = NULL,
|
||||
.exit_on_app_close = false,
|
||||
.stop_app = false,
|
||||
.angle = NULL,
|
||||
.vd_destroy_content = true,
|
||||
.vd_system_decorations = true,
|
||||
|
|
|
|||
|
|
@ -325,6 +325,8 @@ struct scrcpy_options {
|
|||
bool audio_dup;
|
||||
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||
const char *start_app;
|
||||
bool exit_on_app_close;
|
||||
bool stop_app;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
};
|
||||
|
|
|
|||
243
app/src/scrcpy.c
243
app/src/scrcpy.c
|
|
@ -7,6 +7,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include "adb/adb.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// not needed here, but winsock2.h must never be included AFTER windows.h
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
#include "uhid/gamepad_uhid.h"
|
||||
#include "uhid/keyboard_uhid.h"
|
||||
#include "uhid/mouse_uhid.h"
|
||||
|
||||
#ifdef HAVE_USB
|
||||
# include "usb/aoa_hid.h"
|
||||
# include "usb/gamepad_aoa.h"
|
||||
|
|
@ -36,11 +38,13 @@
|
|||
# include "usb/mouse_aoa.h"
|
||||
# include "usb/usb.h"
|
||||
#endif
|
||||
|
||||
#include "util/acksync.h"
|
||||
#include "util/log.h"
|
||||
#include "util/rand.h"
|
||||
#include "util/timeout.h"
|
||||
#include "util/tick.h"
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
# include "v4l2_sink.h"
|
||||
#endif
|
||||
|
|
@ -176,11 +180,15 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
|
|||
}
|
||||
|
||||
static enum scrcpy_exit_code
|
||||
event_loop(struct scrcpy *s, bool has_screen) {
|
||||
event_loop(struct scrcpy *s, bool has_screen, const struct scrcpy_options *options) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SC_EVENT_DEVICE_DISCONNECTED:
|
||||
if (options && options->exit_on_app_close) {
|
||||
LOGI("Target app closed; exiting");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
}
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SC_EVENT_DEMUXER_ERROR:
|
||||
|
|
@ -261,7 +269,7 @@ await_for_server(bool *connected) {
|
|||
|
||||
static void
|
||||
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
||||
void *userdata) {
|
||||
void *userdata) {
|
||||
(void) recorder;
|
||||
(void) userdata;
|
||||
|
||||
|
|
@ -272,7 +280,7 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
|||
|
||||
static void
|
||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
|
|
@ -288,7 +296,7 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
|||
|
||||
static void
|
||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
(void) demuxer;
|
||||
|
||||
const struct scrcpy_options *options = userdata;
|
||||
|
|
@ -299,14 +307,14 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
|||
sc_push_event(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else if (status == SC_DEMUXER_STATUS_ERROR
|
||||
|| (status == SC_DEMUXER_STATUS_DISABLED
|
||||
&& options->require_audio)) {
|
||||
&& options->require_audio)) {
|
||||
sc_push_event(SC_EVENT_DEMUXER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_controller_on_ended(struct sc_controller *controller, bool error,
|
||||
void *userdata) {
|
||||
void *userdata) {
|
||||
// Note: this function may be called twice, once from the controller thread
|
||||
// and once from the receiver thread
|
||||
(void) controller;
|
||||
|
|
@ -422,64 +430,65 @@ scrcpy(struct scrcpy_options *options) {
|
|||
uint32_t scid = scrcpy_generate_scid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.scid = scid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.video_codec = options->video_codec,
|
||||
.audio_codec = options->audio_codec,
|
||||
.video_source = options->video_source,
|
||||
.audio_source = options->audio_source,
|
||||
.camera_facing = options->camera_facing,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
.tunnel_port = options->tunnel_port,
|
||||
.max_size = options->max_size,
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.angle = options->angle,
|
||||
.screen_off_timeout = options->screen_off_timeout,
|
||||
.capture_orientation = options->capture_orientation,
|
||||
.capture_orientation_lock = options->capture_orientation_lock,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
.display_ime_policy = options->display_ime_policy,
|
||||
.video = options->video,
|
||||
.audio = options->audio,
|
||||
.audio_dup = options->audio_dup,
|
||||
.show_touches = options->show_touches,
|
||||
.stay_awake = options->stay_awake,
|
||||
.video_codec_options = options->video_codec_options,
|
||||
.audio_codec_options = options->audio_codec_options,
|
||||
.video_encoder = options->video_encoder,
|
||||
.audio_encoder = options->audio_encoder,
|
||||
.camera_id = options->camera_id,
|
||||
.camera_size = options->camera_size,
|
||||
.camera_ar = options->camera_ar,
|
||||
.camera_fps = options->camera_fps,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
.downsize_on_error = options->downsize_on_error,
|
||||
.tcpip = options->tcpip,
|
||||
.tcpip_dst = options->tcpip_dst,
|
||||
.cleanup = options->cleanup,
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.vd_destroy_content = options->vd_destroy_content,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
.scid = scid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.video_codec = options->video_codec,
|
||||
.audio_codec = options->audio_codec,
|
||||
.video_source = options->video_source,
|
||||
.audio_source = options->audio_source,
|
||||
.camera_facing = options->camera_facing,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
.tunnel_port = options->tunnel_port,
|
||||
.max_size = options->max_size,
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.angle = options->angle,
|
||||
.screen_off_timeout = options->screen_off_timeout,
|
||||
.capture_orientation = options->capture_orientation,
|
||||
.capture_orientation_lock = options->capture_orientation_lock,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
.display_ime_policy = options->display_ime_policy,
|
||||
.video = options->video,
|
||||
.audio = options->audio,
|
||||
.audio_dup = options->audio_dup,
|
||||
.show_touches = options->show_touches,
|
||||
.stay_awake = options->stay_awake,
|
||||
.video_codec_options = options->video_codec_options,
|
||||
.audio_codec_options = options->audio_codec_options,
|
||||
.video_encoder = options->video_encoder,
|
||||
.audio_encoder = options->audio_encoder,
|
||||
.camera_id = options->camera_id,
|
||||
.camera_size = options->camera_size,
|
||||
.camera_ar = options->camera_ar,
|
||||
.camera_fps = options->camera_fps,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
.downsize_on_error = options->downsize_on_error,
|
||||
.tcpip = options->tcpip,
|
||||
.tcpip_dst = options->tcpip_dst,
|
||||
.cleanup = options->cleanup,
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.vd_destroy_content = options->vd_destroy_content,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.exit_on_app_close = options->exit_on_app_close,
|
||||
.list = options->list,
|
||||
};
|
||||
|
||||
static const struct sc_server_callbacks cbs = {
|
||||
.on_connection_failed = sc_server_on_connection_failed,
|
||||
.on_connected = sc_server_on_connected,
|
||||
.on_disconnected = sc_server_on_disconnected,
|
||||
.on_connection_failed = sc_server_on_connection_failed,
|
||||
.on_connected = sc_server_on_connected,
|
||||
.on_disconnected = sc_server_on_disconnected,
|
||||
};
|
||||
if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
|
|
@ -566,7 +575,7 @@ scrcpy(struct scrcpy_options *options) {
|
|||
|
||||
if (options->video_playback && options->control) {
|
||||
if (!sc_file_pusher_init(&s->file_pusher, serial,
|
||||
options->push_target)) {
|
||||
options->push_target)) {
|
||||
goto end;
|
||||
}
|
||||
fp = &s->file_pusher;
|
||||
|
|
@ -575,18 +584,18 @@ scrcpy(struct scrcpy_options *options) {
|
|||
|
||||
if (options->video) {
|
||||
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||
.on_ended = sc_video_demuxer_on_ended,
|
||||
.on_ended = sc_video_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
|
||||
&video_demuxer_cbs, NULL);
|
||||
&video_demuxer_cbs, NULL);
|
||||
}
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||
.on_ended = sc_audio_demuxer_on_ended,
|
||||
.on_ended = sc_audio_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||
&audio_demuxer_cbs, options);
|
||||
&audio_demuxer_cbs, options);
|
||||
}
|
||||
|
||||
bool needs_video_decoder = options->video_playback;
|
||||
|
|
@ -597,22 +606,22 @@ scrcpy(struct scrcpy_options *options) {
|
|||
if (needs_video_decoder) {
|
||||
sc_decoder_init(&s->video_decoder, "video");
|
||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||
&s->video_decoder.packet_sink);
|
||||
&s->video_decoder.packet_sink);
|
||||
}
|
||||
if (needs_audio_decoder) {
|
||||
sc_decoder_init(&s->audio_decoder, "audio");
|
||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||
&s->audio_decoder.packet_sink);
|
||||
&s->audio_decoder.packet_sink);
|
||||
}
|
||||
|
||||
if (options->record_filename) {
|
||||
static const struct sc_recorder_callbacks recorder_cbs = {
|
||||
.on_ended = sc_recorder_on_ended,
|
||||
.on_ended = sc_recorder_on_ended,
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->video,
|
||||
options->audio, options->record_orientation,
|
||||
&recorder_cbs, NULL)) {
|
||||
options->record_format, options->video,
|
||||
options->audio, options->record_orientation,
|
||||
&recorder_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
recorder_initialized = true;
|
||||
|
|
@ -624,11 +633,11 @@ scrcpy(struct scrcpy_options *options) {
|
|||
|
||||
if (options->video) {
|
||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||
&s->recorder.video_packet_sink);
|
||||
&s->recorder.video_packet_sink);
|
||||
}
|
||||
if (options->audio) {
|
||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||
&s->recorder.audio_packet_sink);
|
||||
&s->recorder.audio_packet_sink);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -639,11 +648,11 @@ scrcpy(struct scrcpy_options *options) {
|
|||
|
||||
if (options->control) {
|
||||
static const struct sc_controller_callbacks controller_cbs = {
|
||||
.on_ended = sc_controller_on_ended,
|
||||
.on_ended = sc_controller_on_ended,
|
||||
};
|
||||
|
||||
if (!sc_controller_init(&s->controller, s->server.control_socket,
|
||||
&controller_cbs, NULL)) {
|
||||
&controller_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
controller_initialized = true;
|
||||
|
|
@ -751,8 +760,8 @@ aoa_complete:
|
|||
|
||||
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) {
|
||||
sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller,
|
||||
options->key_inject_mode,
|
||||
options->forward_key_repeat);
|
||||
options->key_inject_mode,
|
||||
options->forward_key_repeat);
|
||||
kp = &s->keyboard_sdk.key_processor;
|
||||
} else if (options->keyboard_input_mode
|
||||
== SC_KEYBOARD_INPUT_MODE_UHID) {
|
||||
|
|
@ -766,7 +775,7 @@ aoa_complete:
|
|||
|
||||
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
|
||||
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller,
|
||||
options->mouse_hover);
|
||||
options->mouse_hover);
|
||||
mp = &s->mouse_sdk.mouse_processor;
|
||||
} else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) {
|
||||
bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller);
|
||||
|
|
@ -800,30 +809,30 @@ aoa_complete:
|
|||
|
||||
if (options->window) {
|
||||
const char *window_title =
|
||||
options->window_title ? options->window_title : info->device_name;
|
||||
options->window_title ? options->window_title : info->device_name;
|
||||
|
||||
struct sc_screen_params screen_params = {
|
||||
.video = options->video_playback,
|
||||
.controller = controller,
|
||||
.fp = fp,
|
||||
.kp = kp,
|
||||
.mp = mp,
|
||||
.gp = gp,
|
||||
.mouse_bindings = options->mouse_bindings,
|
||||
.legacy_paste = options->legacy_paste,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
.shortcut_mods = options->shortcut_mods,
|
||||
.window_title = window_title,
|
||||
.always_on_top = options->always_on_top,
|
||||
.window_x = options->window_x,
|
||||
.window_y = options->window_y,
|
||||
.window_width = options->window_width,
|
||||
.window_height = options->window_height,
|
||||
.window_borderless = options->window_borderless,
|
||||
.orientation = options->display_orientation,
|
||||
.mipmaps = options->mipmaps,
|
||||
.fullscreen = options->fullscreen,
|
||||
.start_fps_counter = options->start_fps_counter,
|
||||
.video = options->video_playback,
|
||||
.controller = controller,
|
||||
.fp = fp,
|
||||
.kp = kp,
|
||||
.mp = mp,
|
||||
.gp = gp,
|
||||
.mouse_bindings = options->mouse_bindings,
|
||||
.legacy_paste = options->legacy_paste,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
.shortcut_mods = options->shortcut_mods,
|
||||
.window_title = window_title,
|
||||
.always_on_top = options->always_on_top,
|
||||
.window_x = options->window_x,
|
||||
.window_y = options->window_y,
|
||||
.window_width = options->window_width,
|
||||
.window_height = options->window_height,
|
||||
.window_borderless = options->window_borderless,
|
||||
.orientation = options->display_orientation,
|
||||
.mipmaps = options->mipmaps,
|
||||
.fullscreen = options->fullscreen,
|
||||
.start_fps_counter = options->start_fps_counter,
|
||||
};
|
||||
|
||||
if (!sc_screen_init(&s->screen, &screen_params)) {
|
||||
|
|
@ -835,7 +844,7 @@ aoa_complete:
|
|||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||
if (options->video_buffer) {
|
||||
sc_delay_buffer_init(&s->video_buffer,
|
||||
options->video_buffer, true);
|
||||
options->video_buffer, true);
|
||||
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink);
|
||||
src = &s->video_buffer.frame_source;
|
||||
}
|
||||
|
|
@ -846,9 +855,9 @@ aoa_complete:
|
|||
|
||||
if (options->audio_playback) {
|
||||
sc_audio_player_init(&s->audio_player, options->audio_buffer,
|
||||
options->audio_output_buffer);
|
||||
options->audio_output_buffer);
|
||||
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
|
||||
&s->audio_player.frame_sink);
|
||||
&s->audio_player.frame_sink);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
|
|
@ -909,7 +918,7 @@ aoa_complete:
|
|||
|
||||
sc_tick deadline = sc_tick_now() + options->time_limit;
|
||||
static const struct sc_timeout_callbacks cbs = {
|
||||
.on_timeout = sc_timeout_on_timeout,
|
||||
.on_timeout = sc_timeout_on_timeout,
|
||||
};
|
||||
|
||||
ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL);
|
||||
|
|
@ -944,8 +953,28 @@ aoa_complete:
|
|||
}
|
||||
}
|
||||
|
||||
ret = event_loop(s, options->window);
|
||||
ret = event_loop(s, options->window, options);
|
||||
terminate_event_loop();
|
||||
|
||||
|
||||
if (options->stop_app) {
|
||||
LOGI("Stopping app [%s]", options->start_app);
|
||||
const char *cmd[512];
|
||||
cmd[0] = sc_adb_get_executable();
|
||||
cmd[1] = "shell";
|
||||
int idx = 0;
|
||||
if (options->serial) {
|
||||
idx = 2;
|
||||
cmd[2] = "-s";
|
||||
cmd[3] = options->serial;
|
||||
}
|
||||
cmd[idx + 2] = "am";
|
||||
cmd[idx + 3] = "force-stop";
|
||||
cmd[idx + 4] = options->start_app;
|
||||
cmd[idx + 5] = NULL;
|
||||
sc_adb_execute(cmd, 0);
|
||||
}
|
||||
|
||||
LOGD("quit...");
|
||||
|
||||
if (options->video_playback) {
|
||||
|
|
@ -955,7 +984,7 @@ aoa_complete:
|
|||
sc_screen_hide_window(&s->screen);
|
||||
}
|
||||
|
||||
end:
|
||||
end:
|
||||
if (timeout_started) {
|
||||
sc_timeout_stop(&s->timeout);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -417,6 +417,9 @@ execute_server(struct sc_server *server,
|
|||
if (!params->vd_system_decorations) {
|
||||
ADD_PARAM("vd_system_decorations=false");
|
||||
}
|
||||
if (params->exit_on_app_close) {
|
||||
ADD_PARAM("exit_on_app_close=true");
|
||||
}
|
||||
if (params->list & SC_OPTION_LIST_ENCODERS) {
|
||||
ADD_PARAM("list_encoders=true");
|
||||
}
|
||||
|
|
@ -752,7 +755,6 @@ sc_server_on_terminated(void *userdata) {
|
|||
sc_intr_interrupt(&server->intr);
|
||||
|
||||
server->cbs->on_disconnected(server, server->cbs_userdata);
|
||||
|
||||
LOGD("Server terminated");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ struct sc_server_params {
|
|||
bool camera_high_speed;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
bool exit_on_app_close;
|
||||
uint8_t list;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -149,28 +149,28 @@ scrcpy --list-apps
|
|||
|
||||
An app, selected by its package name, can be launched on start:
|
||||
|
||||
```
|
||||
```bash
|
||||
scrcpy --start-app=org.mozilla.firefox
|
||||
```
|
||||
|
||||
This feature can be used to run an app in a [virtual
|
||||
display](virtual_display.md):
|
||||
|
||||
```
|
||||
```bash
|
||||
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||
```
|
||||
|
||||
The app can be optionally forced-stop before being started, by adding a `+`
|
||||
prefix:
|
||||
|
||||
```
|
||||
```bash
|
||||
scrcpy --start-app=+org.mozilla.firefox
|
||||
```
|
||||
|
||||
For convenience, it is also possible to select an app by its name, by adding a
|
||||
`?` prefix:
|
||||
|
||||
```
|
||||
```bash
|
||||
scrcpy --start-app=?firefox
|
||||
```
|
||||
|
||||
|
|
@ -179,6 +179,23 @@ passing the package name is recommended.
|
|||
|
||||
The `+` and `?` prefixes can be combined (in that order):
|
||||
|
||||
```
|
||||
```bash
|
||||
scrcpy --start-app=+?firefox
|
||||
```
|
||||
|
||||
Also, if you add a '-' as a suffix, scrcpy will exit when the app is closed, alike `--exit-on-close` (see below):
|
||||
|
||||
```bash
|
||||
scrcpy --start-app=org.mozilla.firefox-
|
||||
```
|
||||
|
||||
### Exit when started app closes
|
||||
|
||||
If you want scrcpy to exit automatically when the started app closes, use the `--exit-on-close` option together with `--start-app`:
|
||||
|
||||
```bash
|
||||
scrcpy --start-app=<package> --exit-on-close
|
||||
```
|
||||
|
||||
- This is useful for kiosk, automation, or demo scenarios.
|
||||
- `--exit-on-close` only works when used with `--start-app`.
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public class Options {
|
|||
private NewDisplay newDisplay;
|
||||
private boolean vdDestroyContent = true;
|
||||
private boolean vdSystemDecorations = true;
|
||||
private boolean exitOnAppClose = false;
|
||||
|
||||
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
|
||||
private Orientation captureOrientation = Orientation.Orient0;
|
||||
|
|
@ -248,6 +249,10 @@ public class Options {
|
|||
return vdSystemDecorations;
|
||||
}
|
||||
|
||||
public boolean getExitOnAppClose() {
|
||||
return exitOnAppClose;
|
||||
}
|
||||
|
||||
public boolean getList() {
|
||||
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
|
||||
}
|
||||
|
|
@ -483,6 +488,9 @@ public class Options {
|
|||
case "vd_system_decorations":
|
||||
options.vdSystemDecorations = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "exit_on_app_close":
|
||||
options.exitOnAppClose = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "capture_orientation":
|
||||
Pair<Orientation.Lock, Orientation> pair = parseCaptureOrientation(value);
|
||||
options.captureOrientationLock = pair.first;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Monitor the state of a running Android application.
|
||||
* This class monitors whether a specific app is still running on a specific display
|
||||
* and notifies when the app is no longer active on that display.
|
||||
*/
|
||||
public class AppMonitor {
|
||||
|
||||
private final String packageName;
|
||||
private final Runnable onAppClosed;
|
||||
private final ScheduledExecutorService executor;
|
||||
private ScheduledFuture<?> scheduledTask;
|
||||
private volatile boolean running;
|
||||
private int targetDisplayId = -1; // -1 means any display
|
||||
private TaskStackMonitor taskStackMonitor;
|
||||
private static final Pattern DISPLAY_PATTERN = Pattern.compile("Display #(\\d+)");
|
||||
// Match visible activities/tasks lines within a display block
|
||||
// Example: "* Task{73b87a7 #325 type=standard A=10150:com.android.chrome U=0 visible=true ...}"
|
||||
private static final Pattern VISIBLE_TASK_PATTERN = Pattern.compile(
|
||||
"(?:\\*\\s+)?Task\\{[^}]*A=\\d+:([^\\s]+)\\s+U=\\d+[^}]*\\bvisible=true\\b");
|
||||
|
||||
public AppMonitor(String packageName, Runnable onAppClosed) {
|
||||
this.packageName = packageName;
|
||||
this.onAppClosed = onAppClosed;
|
||||
this.executor = Executors.newSingleThreadScheduledExecutor();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
// Adaptive scheduling
|
||||
private static final long DELAY_FOUND_MS = 4000;
|
||||
private static final long DELAY_FAST_MS = 1000;
|
||||
private static final int MISSES_TO_CONFIRM = 3;
|
||||
private int consecutiveMisses = 0;
|
||||
|
||||
public AppMonitor(String packageName, int displayId, Runnable onAppClosed) {
|
||||
this.packageName = packageName;
|
||||
this.targetDisplayId = displayId;
|
||||
this.onAppClosed = onAppClosed;
|
||||
this.executor = Executors.newSingleThreadScheduledExecutor();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring the application.
|
||||
*/
|
||||
public void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
|
||||
running = true;
|
||||
Ln.i("AppMonitor started for " + packageName + (targetDisplayId >= 0 ? (" on display " + targetDisplayId) : ""));
|
||||
|
||||
boolean listenerOk = registerTaskStackListener();
|
||||
if (listenerOk) {
|
||||
// Kick an immediate check; subsequent checks happen on task changes or backoff
|
||||
scheduleNext(0);
|
||||
} else {
|
||||
// Fallback: start polling immediately
|
||||
scheduleNext(DELAY_FAST_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring the application.
|
||||
*/
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
running = false;
|
||||
Ln.i("AppMonitor stopped for " + packageName);
|
||||
executor.shutdown();
|
||||
if (taskStackMonitor != null) {
|
||||
taskStackMonitor.stop();
|
||||
taskStackMonitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the monitored app is still running on the target display.
|
||||
*/
|
||||
private void checkAppStatus() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Reduce I/O: keep display headers and task lines containing the package
|
||||
String grepPkg = packageName.replace("'", "'\\''");
|
||||
String result = execCommand(
|
||||
"dumpsys activity activities | grep -E '^(Display #)|(Task).*" + grepPkg + "'"
|
||||
);
|
||||
if (result == null || result.trim().isEmpty()) {
|
||||
// Fallback without grep (in case grep is unavailable)
|
||||
result = execCommand("dumpsys activity activities");
|
||||
}
|
||||
int status = -1; // 1 found, 0 not found but target seen, -1 target not seen
|
||||
|
||||
if (result != null && !result.trim().isEmpty()) {
|
||||
status = isAppRunningOnDisplay(result, packageName, targetDisplayId);
|
||||
}
|
||||
|
||||
if (status == 1) {
|
||||
consecutiveMisses = 0;
|
||||
scheduleNext(DELAY_FOUND_MS);
|
||||
} else if (status == 0) {
|
||||
if (++consecutiveMisses >= MISSES_TO_CONFIRM) {
|
||||
Ln.i("Target app no longer visible on display " + targetDisplayId + ", exiting");
|
||||
running = false;
|
||||
executor.shutdown();
|
||||
onAppClosed.run();
|
||||
} else {
|
||||
scheduleNext(DELAY_FAST_MS);
|
||||
}
|
||||
} else { // target display block not found; do not count as miss, retry fast
|
||||
scheduleNext(DELAY_FAST_MS);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.w("Error checking app status: " + e.getMessage());
|
||||
// On error, retry fast to recover
|
||||
scheduleNext(DELAY_FAST_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleNext(long delayMs) {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (scheduledTask != null && !scheduledTask.isDone()) {
|
||||
scheduledTask.cancel(false);
|
||||
}
|
||||
scheduledTask = executor.schedule(this::checkAppStatus, delayMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private boolean registerTaskStackListener() {
|
||||
try {
|
||||
taskStackMonitor = new TaskStackMonitor(() -> {
|
||||
// On task stack changes, reschedule a fast check (debounced by our own logic)
|
||||
scheduleNext(DELAY_FAST_MS);
|
||||
});
|
||||
return taskStackMonitor.start();
|
||||
} catch (Throwable t) {
|
||||
Ln.w("TaskStackMonitor unavailable: " + t.getMessage());
|
||||
taskStackMonitor = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dumpsys activity activities output to check if the app is running on the target display.
|
||||
*/
|
||||
private int isAppRunningOnDisplay(String dumpsysOutput, String packageName, int targetDisplayId) {
|
||||
String[] lines = dumpsysOutput.split("\n");
|
||||
int currentDisplayId = -1;
|
||||
boolean inTargetBlock = targetDisplayId < 0; // if any display, become true on first display
|
||||
boolean seenTargetDisplay = (targetDisplayId < 0);
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
|
||||
// Check for display information
|
||||
Matcher displayMatcher = DISPLAY_PATTERN.matcher(line);
|
||||
if (displayMatcher.find()) {
|
||||
try {
|
||||
currentDisplayId = Integer.parseInt(displayMatcher.group(1));
|
||||
} catch (NumberFormatException e) {
|
||||
Ln.w("Could not parse display ID: " + displayMatcher.group(1));
|
||||
}
|
||||
inTargetBlock = (targetDisplayId < 0) || (currentDisplayId == targetDisplayId);
|
||||
if (currentDisplayId == targetDisplayId) {
|
||||
seenTargetDisplay = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Within the relevant display section, check visible tasks
|
||||
if (currentDisplayId != -1 && inTargetBlock) {
|
||||
Matcher taskMatcher = VISIBLE_TASK_PATTERN.matcher(line);
|
||||
if (taskMatcher.find()) {
|
||||
String taskPackage = taskMatcher.group(1);
|
||||
if (packageName.equals(taskPackage)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a specific display is targeted but its block is not present,
|
||||
// consider the app not visible there (counts as a miss, debounced).
|
||||
if (targetDisplayId >= 0 && !seenTargetDisplay) {
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell command and return the output.
|
||||
*/
|
||||
private String execCommand(String command) {
|
||||
try {
|
||||
Process process = new ProcessBuilder("sh", "-c", command)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(process.getInputStream()));
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
}
|
||||
|
||||
String result = output.toString();
|
||||
if (exitCode != 0) {
|
||||
Ln.w("Command '" + command + "' exited with code " + exitCode);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Ln.w("Failed to execute command: " + command + ", error: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
private ExecutorService startAppExecutor;
|
||||
private AppMonitor appMonitor;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
|
|
@ -82,6 +83,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
private final DeviceMessageSender sender;
|
||||
private final boolean clipboardAutosync;
|
||||
private final boolean powerOn;
|
||||
private final boolean exitOnAppClose;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
|
|
@ -106,6 +108,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
this.cleanUp = cleanUp;
|
||||
this.clipboardAutosync = options.getClipboardAutosync();
|
||||
this.powerOn = options.getPowerOn();
|
||||
this.exitOnAppClose = options.getExitOnAppClose();
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(controlChannel);
|
||||
|
||||
|
|
@ -691,6 +694,33 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
|
||||
Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "...");
|
||||
Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart);
|
||||
|
||||
// Start monitoring the app if exit_on_app_close is enabled
|
||||
if (exitOnAppClose) {
|
||||
if (appMonitor != null) {
|
||||
appMonitor.stop();
|
||||
}
|
||||
// Pass the display ID to monitor the app on the specific display
|
||||
appMonitor = new AppMonitor(app.getPackageName(), startAppDisplayId, this::onAppClosed);
|
||||
appMonitor.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void onAppClosed() {
|
||||
Ln.i("Monitored app closed, requesting scrcpy exit");
|
||||
// Stop the app monitor
|
||||
if (appMonitor != null) {
|
||||
appMonitor.stop();
|
||||
appMonitor = null;
|
||||
}
|
||||
|
||||
// Terminate the server process to close the connection
|
||||
// This will cause the client to detect the disconnection and exit
|
||||
try {
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
Ln.w("Error handling app close: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private int getStartAppDisplayId() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* Registers a TaskStackListener via reflection (hidden API) to be notified when the
|
||||
* task stack changes. Falls back to polling if registration fails.
|
||||
*/
|
||||
public final class TaskStackMonitor {
|
||||
|
||||
public interface Listener {
|
||||
void onTaskStackChanged();
|
||||
}
|
||||
|
||||
private final Listener listener;
|
||||
private Object activityTaskManager; // IActivityTaskManager
|
||||
private Object taskStackListenerProxy; // ITaskStackListener
|
||||
private Method registerMethod;
|
||||
private Method unregisterMethod;
|
||||
|
||||
public TaskStackMonitor(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
try {
|
||||
// android.app.ActivityTaskManager.getService()
|
||||
Class<?> atmClazz = Class.forName("android.app.ActivityTaskManager");
|
||||
Method getService = atmClazz.getDeclaredMethod("getService");
|
||||
activityTaskManager = getService.invoke(null);
|
||||
if (activityTaskManager == null) {
|
||||
Ln.w("ActivityTaskManager.getService() returned null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Interface android.app.ITaskStackListener
|
||||
Class<?> listenerInterface = Class.forName("android.app.ITaskStackListener");
|
||||
|
||||
// Create dynamic proxy for ITaskStackListener
|
||||
taskStackListenerProxy = Proxy.newProxyInstance(
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new Class<?>[] { listenerInterface },
|
||||
new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
String name = method.getName();
|
||||
if ("onTaskStackChanged".equals(name)
|
||||
|| "onTaskMovedToFront".equals(name)
|
||||
|| "onTaskFocusChanged".equals(name)
|
||||
|| "onTaskStackChangedBackground".equals(name)) {
|
||||
try {
|
||||
listener.onTaskStackChanged();
|
||||
} catch (Throwable t) {
|
||||
Ln.w("TaskStack listener callback error: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Methods: register/unregisterTaskStackListener(ITaskStackListener)
|
||||
registerMethod = activityTaskManager.getClass().getMethod(
|
||||
"registerTaskStackListener", listenerInterface);
|
||||
unregisterMethod = activityTaskManager.getClass().getMethod(
|
||||
"unregisterTaskStackListener", listenerInterface);
|
||||
|
||||
registerMethod.invoke(activityTaskManager, taskStackListenerProxy);
|
||||
Ln.i("TaskStackMonitor registered");
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Ln.w("Could not register TaskStackListener: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
if (activityTaskManager != null && taskStackListenerProxy != null && unregisterMethod != null) {
|
||||
unregisterMethod.invoke(activityTaskManager, taskStackListenerProxy);
|
||||
Ln.i("TaskStackMonitor unregistered");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Ln.w("Could not unregister TaskStackListener: " + e.getMessage());
|
||||
}
|
||||
activityTaskManager = null;
|
||||
taskStackListenerProxy = null;
|
||||
registerMethod = null;
|
||||
unregisterMethod = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue