diff --git a/app/data/disconnected.png b/app/data/disconnected.png new file mode 100644 index 0000000000..37ff3d855e Binary files /dev/null and b/app/data/disconnected.png differ diff --git a/app/data/icon.png b/app/data/scrcpy.png similarity index 100% rename from app/data/icon.png rename to app/data/scrcpy.png diff --git a/app/data/icon.svg b/app/data/scrcpy.svg similarity index 100% rename from app/data/icon.svg rename to app/data/scrcpy.svg diff --git a/app/meson.build b/app/meson.build index 35c2e3eac9..b0f6a37af6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,6 +15,7 @@ src = [ 'src/delay_buffer.c', 'src/demuxer.c', 'src/device_msg.c', + 'src/disconnect.c', 'src/events.c', 'src/icon.c', 'src/file_pusher.c', @@ -191,8 +192,7 @@ executable('scrcpy', src, datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') -install_data('data/icon.png', - rename: 'scrcpy.png', +install_data('data/scrcpy.png', install_dir: datadir / 'icons/hicolor/256x256/apps') install_data('data/zsh-completion/_scrcpy', install_dir: datadir / 'zsh/site-functions') @@ -289,6 +289,6 @@ endif if meson.version().version_compare('>= 0.58.0') devenv = environment() - devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png') + devenv.set('SCRCPY_ICON_DIR', meson.current_source_dir() / 'data') meson.add_devenv(devenv) endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 20b1e14217..f0848077c5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -851,8 +851,8 @@ Path to adb. Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. .TP -.B SCRCPY_ICON_PATH -Path to the program icon. +.B SCRCPY_ICON_DIR +Path to the icon directory. .TP .B SCRCPY_SERVER_PATH diff --git a/app/src/cli.c b/app/src/cli.c index 9373bdbd6f..bf7297bb93 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1249,8 +1249,8 @@ static const struct sc_envvar envvars[] = { "--tcpip=) is specified", }, { - .name = "SCRCPY_ICON_PATH", - .text = "Path to the program icon", + .name = "SCRCPY_ICON_DIR", + .text = "Path to the icon directory", }, { .name = "SCRCPY_SERVER_PATH", diff --git a/app/src/disconnect.c b/app/src/disconnect.c new file mode 100644 index 0000000000..d494091b03 --- /dev/null +++ b/app/src/disconnect.c @@ -0,0 +1,88 @@ +#include "disconnect.h" + +#include + +#include "icon.h" +#include "util/log.h" + +static int +run(void *userdata) { + struct sc_disconnect *d = userdata; + + SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_DISCONNECTED); + if (icon) { + d->cbs->on_icon_loaded(d, icon, d->cbs_userdata); + } else { + LOGE("Could not load disconnected icon"); + } + + sc_mutex_lock(&d->mutex); + bool timed_out = false; + while (!d->interrupted && !timed_out) { + timed_out = !sc_cond_timedwait(&d->cond, &d->mutex, d->deadline); + } + bool interrupted = d->interrupted; + sc_mutex_unlock(&d->mutex); + + if (!interrupted) { + d->cbs->on_timeout(d, d->cbs_userdata); + } + + return 0; +} + +bool +sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline, + const struct sc_disconnect_callbacks *cbs, + void *cbs_userdata) { + bool ok = sc_mutex_init(&d->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&d->cond); + if (!ok) { + goto error_destroy_mutex; + } + + d->deadline = deadline; + d->interrupted = false; + + assert(cbs && cbs->on_icon_loaded && cbs->on_timeout); + d->cbs = cbs; + d->cbs_userdata = cbs_userdata; + + ok = sc_thread_create(&d->thread, run, "scrcpy-dis", d); + if (!ok) { + goto error_destroy_cond; + } + + return true; + +error_destroy_cond: + sc_cond_destroy(&d->cond); +error_destroy_mutex: + sc_mutex_destroy(&d->mutex); + + return false; +} + +void +sc_disconnect_interrupt(struct sc_disconnect *d) { + sc_mutex_lock(&d->mutex); + d->interrupted = true; + sc_mutex_unlock(&d->mutex); + // wake up blocking wait + sc_cond_signal(&d->cond); +} + +void +sc_disconnect_join(struct sc_disconnect *d) { + sc_thread_join(&d->thread, NULL); +} + +void +sc_disconnect_destroy(struct sc_disconnect *d) { + sc_cond_destroy(&d->cond); + sc_mutex_destroy(&d->mutex); +} diff --git a/app/src/disconnect.h b/app/src/disconnect.h new file mode 100644 index 0000000000..5b63ee598e --- /dev/null +++ b/app/src/disconnect.h @@ -0,0 +1,47 @@ +#ifndef SC_DISCONNECT +#define SC_DISCONNECT + +#include "common.h" + +#include "SDL3/SDL_surface.h" +#include "util/tick.h" +#include "util/thread.h" + +// Tool to handle loading the icon and signal timeout when the device is +// unexpectedly disconnected +struct sc_disconnect { + sc_tick deadline; + + struct sc_thread thread; + struct sc_mutex mutex; + struct sc_cond cond; + bool interrupted; + + const struct sc_disconnect_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_disconnect_callbacks { + // Called when the disconnected icon is loaded + void (*on_icon_loaded)(struct sc_disconnect *d, SDL_Surface *icon, + void *userdata); + + // Called when the timeout expired (the scrcpy window must be closed) + void (*on_timeout)(struct sc_disconnect *d, void *userdata); +}; + +bool +sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline, + const struct sc_disconnect_callbacks *cbs, + void *cbs_userdata); + +void +sc_disconnect_interrupt(struct sc_disconnect *d); + +void +sc_disconnect_join(struct sc_disconnect *d); + +void +sc_disconnect_destroy(struct sc_disconnect *d); + +#endif diff --git a/app/src/events.c b/app/src/events.c index ca7e4008a6..5269bdbcf8 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -6,9 +6,13 @@ #include "util/thread.h" bool -sc_push_event_impl(uint32_t type, const char *name) { - SDL_Event event; - event.type = type; +sc_push_event_impl(uint32_t type, void *ptr, const char *name) { + SDL_Event event = { + .user = { + .type = type, + .data1 = ptr, + } + }; bool ok = SDL_PushEvent(&event); if (!ok) { LOGE("Could not post %s event: %s", name, SDL_GetError()); diff --git a/app/src/events.h b/app/src/events.h index 567268ab7f..c82741afe6 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -14,18 +14,20 @@ enum { SC_EVENT_DEVICE_DISCONNECTED, SC_EVENT_SERVER_CONNECTION_FAILED, SC_EVENT_SERVER_CONNECTED, - SC_EVENT_USB_DEVICE_DISCONNECTED, SC_EVENT_DEMUXER_ERROR, SC_EVENT_RECORDER_ERROR, SC_EVENT_TIME_LIMIT_REACHED, SC_EVENT_CONTROLLER_ERROR, SC_EVENT_AOA_OPEN_ERROR, + SC_EVENT_DISCONNECTED_ICON_LOADED, + SC_EVENT_DISCONNECTED_TIMEOUT, }; bool -sc_push_event_impl(uint32_t type, const char *name); +sc_push_event_impl(uint32_t type, void *ptr, const char *name); -#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) +#define sc_push_event(TYPE) sc_push_event_impl(TYPE, NULL, # TYPE) +#define sc_push_event_with_data(TYPE, PTR) sc_push_event_impl(TYPE, PTR, # TYPE) typedef void (*sc_runnable_fn)(void *userdata); diff --git a/app/src/icon.c b/app/src/icon.c index a1e82a5aea..0a84f5ba25 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -14,33 +14,37 @@ #include "config.h" #include "util/env.h" -#ifdef PORTABLE -# include "util/file.h" -#endif +#include "util/file.h" #include "util/log.h" -#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" -#define SCRCPY_DEFAULT_ICON_PATH \ - PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png" +#define SCRCPY_DEFAULT_ICON_DIR PREFIX "/share/icons/hicolor/256x256/apps" static char * -get_icon_path(void) { - char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); - if (icon_path) { +get_icon_path(const char *filename) { + char *icon_path; + + char *icon_dir = sc_get_env("SCRCPY_ICON_DIR"); + if (icon_dir) { // if the envvar is set, use it - LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); + icon_path = sc_file_build_path(icon_dir, filename); + free(icon_dir); + if (!icon_path) { + LOG_OOM(); + return NULL; + } + LOGD("Using icon from SCRCPY_ICON_DIR: %s", icon_path); return icon_path; } #ifndef PORTABLE - LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); - icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + icon_path = sc_file_build_path(SCRCPY_DEFAULT_ICON_DIR, filename); if (!icon_path) { LOG_OOM(); return NULL; } + LOGD("Using icon: %s", icon_path); #else - icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); + icon_path = sc_file_get_local_path(filename); if (!icon_path) { LOGE("Could not get icon path"); return NULL; @@ -177,7 +181,7 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) { } static SDL_Surface * -load_from_path(const char *path) { +sc_icon_load_from_full_path(const char *path) { AVFrame *frame = decode_image(path); if (!frame) { return NULL; @@ -274,19 +278,19 @@ load_from_path(const char *path) { } SDL_Surface * -scrcpy_icon_load(void) { - char *icon_path = get_icon_path(); +sc_icon_load(const char *filename) { + char *icon_path = get_icon_path(filename); if (!icon_path) { return NULL; } - SDL_Surface *icon = load_from_path(icon_path); + SDL_Surface *icon = sc_icon_load_from_full_path(icon_path); free(icon_path); return icon; } void -scrcpy_icon_destroy(SDL_Surface *icon) { +sc_icon_destroy(SDL_Surface *icon) { SDL_PropertiesID props = SDL_GetSurfaceProperties(icon); assert(props); AVFrame *frame = SDL_GetPointerProperty(props, "sc_frame", NULL); diff --git a/app/src/icon.h b/app/src/icon.h index 21fbc5488c..44951cce80 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -5,10 +5,13 @@ #include +#define SC_ICON_FILENAME_SCRCPY "scrcpy.png" +#define SC_ICON_FILENAME_DISCONNECTED "disconnected.png" + SDL_Surface * -scrcpy_icon_load(void); +sc_icon_load(const char *filename); void -scrcpy_icon_destroy(SDL_Surface *icon); +sc_icon_destroy(SDL_Surface *icon); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 6e56d31cc8..6f85aa8b23 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -7,6 +7,7 @@ #include "android/input.h" #include "android/keycodes.h" +#include "events.h" #include "input_events.h" #include "screen.h" #include "shortcut_mod.h" @@ -46,6 +47,8 @@ sc_input_manager_init(struct sc_input_manager *im, im->key_repeat = 0; im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID + + im->disconnected = false; } static void @@ -349,7 +352,7 @@ apply_orientation_transform(struct sc_input_manager *im, static void sc_input_manager_process_text_input(struct sc_input_manager *im, const SDL_TextInputEvent *event) { - if (im->camera || !im->kp || im->screen->paused) { + if (im->camera || !im->kp || im->screen->paused || im->disconnected) { return; } @@ -417,6 +420,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool control = im->controller; bool paused = im->screen->paused; bool video = im->screen->video; + bool disconnected = im->disconnected; SDL_Keycode sdl_keycode = event->key; uint16_t mod = event->mod; @@ -433,7 +437,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod) || sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode); - if (down && !repeat) { + if (down && !repeat && !disconnected) { if (sdl_keycode == im->last_keycode && mod == im->last_mod) { ++im->key_repeat; } else { @@ -510,6 +514,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } + if (disconnected) { + // Only handle shortcuts that do not interact with the device (since + // it is disconnected) + return; + } + // Flatten conditions to avoid additional indentation levels if (control) { // Controls for all sources @@ -718,7 +728,7 @@ sc_input_manager_get_position(struct sc_input_manager *im, int32_t x, static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { - if (im->camera || !im->mp || im->screen->paused) { + if (im->camera || !im->mp || im->screen->paused || im->disconnected) { return; } @@ -757,7 +767,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, static void sc_input_manager_process_touch(struct sc_input_manager *im, const SDL_TouchFingerEvent *event) { - if (im->camera || !im->mp || im->screen->paused) { + if (im->camera || !im->mp || im->screen->paused || im->disconnected) { return; } @@ -812,7 +822,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // some mouse events do not interact with the device, so process the event // even if control is disabled - if (im->camera) { + if (im->camera || im->disconnected) { return; } @@ -983,7 +993,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, static void sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, const SDL_MouseWheelEvent *event) { - if (im->camera || !im->kp || im->screen->paused) { + if (im->camera || !im->kp || im->screen->paused || im->disconnected) { return; } @@ -1013,7 +1023,7 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, const SDL_GamepadDeviceEvent *event) { // Handle device added or removed even if paused - if (im->camera || !im->gp) { + if (im->camera || !im->gp || im->disconnected) { return; } @@ -1058,7 +1068,7 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, static void sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, const SDL_GamepadAxisEvent *event) { - if (im->camera || !im->gp || im->screen->paused) { + if (im->camera || !im->gp || im->screen->paused || im->disconnected) { return; } @@ -1078,7 +1088,7 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, static void sc_input_manager_process_gamepad_button(struct sc_input_manager *im, const SDL_GamepadButtonEvent *event) { - if (im->camera || !im->gp || im->screen->paused) { + if (im->camera || !im->gp || im->screen->paused || im->disconnected) { return; } @@ -1104,7 +1114,7 @@ is_apk(const char *file) { static void sc_input_manager_process_file(struct sc_input_manager *im, const SDL_DropEvent *event) { - if (im->camera || !im->controller) { + if (im->camera || !im->controller || im->disconnected) { return; } @@ -1127,6 +1137,16 @@ sc_input_manager_process_file(struct sc_input_manager *im, } } +static void +sc_input_manager_on_device_disconnected(struct sc_input_manager *im) { + im->disconnected = true; + + struct sc_fps_counter *fps_counter = &im->screen->fps_counter; + if (sc_fps_counter_is_started(fps_counter)) { + sc_fps_counter_stop(fps_counter); + } +} + void sc_input_manager_handle_event(struct sc_input_manager *im, const SDL_Event *event) { @@ -1167,5 +1187,8 @@ sc_input_manager_handle_event(struct sc_input_manager *im, case SDL_EVENT_DROP_FILE: sc_input_manager_process_file(im, &event->drop); break; + case SC_EVENT_DEVICE_DISCONNECTED: + sc_input_manager_on_device_disconnected(im); + break; } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index ed96a3ddad..c986cbc5c4 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -46,6 +46,8 @@ struct sc_input_manager { uint16_t last_mod; uint64_t next_sequence; // used for request acknowledgements + + bool disconnected; }; struct sc_input_manager_params { diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 68fdc226d1..fca52074b8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -173,6 +173,9 @@ event_loop(struct scrcpy *s, bool has_screen) { switch (event.type) { case SC_EVENT_DEVICE_DISCONNECTED: LOGW("Device disconnected"); + if (has_screen) { + sc_screen_handle_event(&s->screen, &event); + } return SCRCPY_EXIT_DISCONNECTED; case SC_EVENT_DEMUXER_ERROR: LOGE("Demuxer error"); @@ -209,17 +212,18 @@ event_loop(struct scrcpy *s, bool has_screen) { } static void -terminate_event_loop(void) { +terminate_runnables_on_event_loop(void) { sc_reject_new_runnables(); SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) { - // Make sure all posted runnables are run, to avoid memory leaks - sc_runnable_fn run = event.user.data1; - void *userdata = event.user.data2; - run(userdata); - } + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, + SC_EVENT_RUN_ON_MAIN_THREAD, + SC_EVENT_RUN_ON_MAIN_THREAD) == 1) { + assert(event.type == SC_EVENT_RUN_ON_MAIN_THREAD); + // Make sure all posted runnables are run, to avoid memory leaks + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); } } @@ -414,6 +418,7 @@ scrcpy(struct scrcpy_options *options) { bool screen_initialized = false; bool timeout_initialized = false; bool timeout_started = false; + bool disconnected = false; struct sc_acksync *acksync = NULL; @@ -946,15 +951,8 @@ scrcpy(struct scrcpy_options *options) { } ret = event_loop(s, options->window); - terminate_event_loop(); - LOGD("quit..."); - - if (options->window) { - // Close the window immediately on closing, because screen_destroy() - // may only be called once the video demuxer thread is joined (it may - // take time) - sc_screen_hide_window(&s->screen); - } + terminate_runnables_on_event_loop(); + disconnected = ret == SCRCPY_EXIT_DISCONNECTED; end: if (timeout_started) { @@ -999,6 +997,17 @@ scrcpy(struct scrcpy_options *options) { sc_server_stop(&s->server); } + if (screen_initialized) { + if (disconnected) { + sc_screen_handle_disconnection(&s->screen); + } + LOGD("Quit..."); + + // Close the window immediately, because sc_screen_destroy() may only be + // called once the video demuxer thread is joined (it may take time) + sc_screen_hide_window(&s->screen); + } + if (timeout_started) { sc_timeout_join(&s->timeout); } diff --git a/app/src/screen.c b/app/src/screen.c index d50b670f89..9f1876ed86 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -184,7 +184,7 @@ compute_content_rect(struct sc_size render_size, struct sc_size content_size, static void sc_screen_update_content_rect(struct sc_screen *screen) { // Only upscale video frames, not icon - bool can_upscale = screen->video; + bool can_upscale = screen->video && !screen->disconnected; struct sc_size render_size = sc_sdl_get_render_output_size(screen->renderer); @@ -366,6 +366,8 @@ sc_screen_init(struct sc_screen *screen, screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; + screen->disconnected = false; + screen->disconnect_started = false; screen->video = params->video; screen->camera = params->camera; @@ -477,7 +479,7 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_texture; } - SDL_Surface *icon = scrcpy_icon_load(); + SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY); if (icon) { if (!SDL_SetWindowIcon(screen->window, icon)) { LOGW("Could not set window icon: %s", SDL_GetError()); @@ -492,7 +494,7 @@ sc_screen_init(struct sc_screen *screen, } } - scrcpy_icon_destroy(icon); + sc_icon_destroy(icon); } else { // not fatal LOGE("Could not load icon"); @@ -626,9 +628,19 @@ sc_screen_interrupt(struct sc_screen *screen) { sc_fps_counter_interrupt(&screen->fps_counter); } +static void +sc_screen_interrupt_disconnect(struct sc_screen *screen) { + if (screen->disconnect_started) { + sc_disconnect_interrupt(&screen->disconnect); + } +} + void sc_screen_join(struct sc_screen *screen) { sc_fps_counter_join(&screen->fps_counter); + if (screen->disconnect_started) { + sc_disconnect_join(&screen->disconnect); + } } void @@ -636,6 +648,9 @@ sc_screen_destroy(struct sc_screen *screen) { #ifndef NDEBUG assert(!screen->open); #endif + if (screen->disconnect_started) { + sc_disconnect_destroy(&screen->disconnect); + } sc_texture_destroy(&screen->tex); av_frame_free(&screen->frame); #ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE @@ -645,6 +660,17 @@ sc_screen_destroy(struct sc_screen *screen) { SDL_DestroyWindow(screen->window); sc_fps_counter_destroy(&screen->fps_counter); sc_frame_buffer_destroy(&screen->fb); + + SDL_Event event; + int nevents = SDL_PeepEvents(&event, 1, SDL_GETEVENT, + SC_EVENT_DISCONNECTED_ICON_LOADED, + SC_EVENT_DISCONNECTED_ICON_LOADED); + if (nevents == 1) { + assert(event.type == SC_EVENT_DISCONNECTED_ICON_LOADED); + // The event was posted, but not handled, the icon must be freed + SDL_Surface *dangling_icon = event.user.data1; + sc_icon_destroy(dangling_icon); + } } static void @@ -857,6 +883,27 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { content_size.height); } +static void +sc_disconnect_on_icon_loaded(struct sc_disconnect *d, SDL_Surface *icon, + void *userdata) { + (void) d; + (void) userdata; + + bool ok = sc_push_event_with_data(SC_EVENT_DISCONNECTED_ICON_LOADED, icon); + if (!ok) { + sc_icon_destroy(icon); + } +} + +static void +sc_disconnect_on_timeout(struct sc_disconnect *d, void *userdata) { + (void) d; + (void) userdata; + + bool ok = sc_push_event(SC_EVENT_DISCONNECTED_TIMEOUT); + (void) ok; // ignore failure +} + void sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { switch (event->type) { @@ -903,6 +950,31 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { apply_pending_resize(screen); sc_screen_render(screen, true); } + return; + case SC_EVENT_DEVICE_DISCONNECTED: + assert(!screen->disconnected); + screen->disconnected = true; + if (!screen->window_shown) { + // No window open + return; + } + + sc_input_manager_handle_event(&screen->im, event); + + sc_texture_reset(&screen->tex); + sc_screen_render(screen, true); + + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_SEC(2); + static const struct sc_disconnect_callbacks cbs = { + .on_icon_loaded = sc_disconnect_on_icon_loaded, + .on_timeout = sc_disconnect_on_timeout, + }; + bool ok = + sc_disconnect_start(&screen->disconnect, deadline, &cbs, NULL); + if (ok) { + screen->disconnect_started = true; + } + return; } @@ -915,6 +987,55 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { sc_input_manager_handle_event(&screen->im, event); } +void +sc_screen_handle_disconnection(struct sc_screen *screen) { + if (!screen->window_shown) { + // No window open, quit immediately + return; + } + + if (!screen->disconnect_started) { + // If sc_disconnect_start() failed, quit immediately + return; + } + + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_EVENT_WINDOW_EXPOSED: + sc_screen_render(screen, true); + break; + case SC_EVENT_DISCONNECTED_ICON_LOADED: { + SDL_Surface *icon_disconnected = event.user.data1; + assert(icon_disconnected); + + bool ok = sc_texture_set_from_surface(&screen->tex, + icon_disconnected); + if (ok) { + screen->content_size.width = icon_disconnected->w; + screen->content_size.height = icon_disconnected->h; + sc_screen_render(screen, true); + } else { + // not fatal + LOGE("Could not set disconnected icon"); + } + + sc_icon_destroy(icon_disconnected); + break; + } + case SC_EVENT_DISCONNECTED_TIMEOUT: + LOGD("Closing after device disconnection"); + return; + case SDL_EVENT_QUIT: + LOGD("User requested to quit"); + sc_screen_interrupt_disconnect(screen); + return; + default: + sc_input_manager_handle_event(&screen->im, &event); + } + } +} + struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { diff --git a/app/src/screen.h b/app/src/screen.h index 4ff835a844..a97a825623 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -12,6 +12,7 @@ #include "controller.h" #include "coords.h" +#include "disconnect.h" #include "fps_counter.h" #include "frame_buffer.h" #include "input_manager.h" @@ -76,6 +77,10 @@ struct sc_screen { bool paused; AVFrame *resume_frame; + + bool disconnected; + bool disconnect_started; + struct sc_disconnect disconnect; }; struct sc_screen_params { @@ -115,7 +120,7 @@ bool sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); // request to interrupt any inner thread -// must be called before screen_join() +// must be called before sc_screen_join() void sc_screen_interrupt(struct sc_screen *screen); @@ -159,6 +164,10 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused); void sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event); +// run the event loop once the device is disconnected +void +sc_screen_handle_disconnection(struct sc_screen *screen); + // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct sc_point diff --git a/app/src/texture.c b/app/src/texture.c index 1a88bda17f..2ce9a47b74 100644 --- a/app/src/texture.c +++ b/app/src/texture.c @@ -225,3 +225,11 @@ sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface) { return true; } + +void +sc_texture_reset(struct sc_texture *tex) { + if (tex->texture) { + SDL_DestroyTexture(tex->texture); + tex->texture = NULL; + } +} diff --git a/app/src/texture.h b/app/src/texture.h index 80587340d6..8c0c0c7152 100644 --- a/app/src/texture.h +++ b/app/src/texture.h @@ -41,4 +41,7 @@ sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame); bool sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface); +void +sc_texture_reset(struct sc_texture *tex); + #endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index c79aee742e..7718920941 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -31,7 +31,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) usb; (void) userdata; - sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } static enum scrcpy_exit_code @@ -39,8 +39,9 @@ event_loop(struct scrcpy_otg *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { - case SC_EVENT_USB_DEVICE_DISCONNECTED: + case SC_EVENT_DEVICE_DISCONNECTED: LOGW("Device disconnected"); + sc_screen_handle_event(&s->screen, &event); return SCRCPY_EXIT_DISCONNECTED; case SC_EVENT_AOA_OPEN_ERROR: LOGE("AOA open error"); @@ -96,6 +97,7 @@ scrcpy_otg(struct scrcpy_options *options) { bool aoa_started = false; bool aoa_initialized = false; bool screen_initialized = false; + bool disconnected = false; #ifdef _WIN32 // On Windows, only one process could open a USB device @@ -221,7 +223,7 @@ scrcpy_otg(struct scrcpy_options *options) { usb_device_initialized = false; ret = event_loop(s); - LOGD("quit..."); + disconnected = ret == SCRCPY_EXIT_DISCONNECTED; end: if (aoa_started) { @@ -231,6 +233,14 @@ scrcpy_otg(struct scrcpy_options *options) { if (screen_initialized) { sc_screen_interrupt(&s->screen); + + if (disconnected) { + sc_screen_handle_disconnection(&s->screen); + } + LOGD("Quit..."); + + // Close the window immediately + sc_screen_hide_window(&s->screen); } if (mp) { diff --git a/app/src/util/file.c b/app/src/util/file.c index 174e5efd8c..83b0d61264 100644 --- a/app/src/util/file.c +++ b/app/src/util/file.c @@ -5,6 +5,25 @@ #include "util/log.h" +char * +sc_file_build_path(const char *dir, const char *name) { + size_t dir_len = strlen(dir); + size_t name_len = strlen(name); + + size_t len = dir_len + name_len + 2; // +2: '/' and '\0' + char *path = malloc(len); + if (!path) { + LOG_OOM(); + return NULL; + } + + memcpy(path, dir, dir_len); + path[dir_len] = SC_PATH_SEPARATOR; + // namelen + 1 to copy the final '\0' + memcpy(&path[dir_len + 1], name, name_len + 1); + return path; +} + char * sc_file_get_local_path(const char *name) { char *executable_path = sc_file_get_executable_path(); @@ -25,24 +44,9 @@ sc_file_get_local_path(const char *name) { *p = '\0'; // modify executable_path in place char *dir = executable_path; - size_t dirlen = strlen(dir); - size_t namelen = strlen(name); - - size_t len = dirlen + namelen + 2; // +2: '/' and '\0' - char *file_path = malloc(len); - if (!file_path) { - LOG_OOM(); - free(executable_path); - return NULL; - } - - memcpy(file_path, dir, dirlen); - file_path[dirlen] = SC_PATH_SEPARATOR; - // namelen + 1 to copy the final '\0' - memcpy(&file_path[dirlen + 1], name, namelen + 1); + char *file_path = sc_file_build_path(dir, name); free(executable_path); return file_path; } - diff --git a/app/src/util/file.h b/app/src/util/file.h index 089f6f75be..56c369d730 100644 --- a/app/src/util/file.h +++ b/app/src/util/file.h @@ -40,6 +40,15 @@ sc_file_get_executable_path(void); char * sc_file_get_local_path(const char *name); +/** + * Return the concatenation of dir, the path separator and the filename. + * + * The result must be freed by the caller using free(). It may return NULL on + * error. + */ +char * +sc_file_build_path(const char *dir, const char *filename); + /** * Indicate if the file exists and is not a directory */ diff --git a/release/build_linux.sh b/release/build_linux.sh index e83d355e00..37dc763bf5 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -41,6 +41,7 @@ ninja -C "$LINUX_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$LINUX_BUILD_DIR/dist" cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/" -cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" +cp app/data/scrcpy.png "$LINUX_BUILD_DIR/dist/" +cp app/data/disconnected.png "$LINUX_BUILD_DIR/dist/" cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh index d6f1a56a49..b8b2436af4 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -41,6 +41,7 @@ ninja -C "$MACOS_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$MACOS_BUILD_DIR/dist" cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/" -cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" +cp app/data/scrcpy.png "$MACOS_BUILD_DIR/dist/" +cp app/data/disconnected.png "$MACOS_BUILD_DIR/dist/" cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" diff --git a/release/build_windows.sh b/release/build_windows.sh index 5bf1d67f60..a38ca725f9 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -49,7 +49,8 @@ ninja -C "$WINXX_BUILD_DIR" mkdir -p "$WINXX_BUILD_DIR/dist" cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/" cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" -cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" +cp app/data/scrcpy.png "$WINXX_BUILD_DIR/dist/" +cp app/data/disconnected.png "$WINXX_BUILD_DIR/dist/" cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/" diff --git a/run b/run index 56f0a4e1c0..abc4c9a09e 100755 --- a/run +++ b/run @@ -20,6 +20,6 @@ then exit 1 fi -SCRCPY_ICON_PATH="app/data/icon.png" \ +SCRCPY_ICON_DIR="app/data" \ SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ "$BUILDDIR/app/scrcpy" "$@"