diff --git a/app/data/disconnected.png b/app/data/disconnected.png new file mode 100644 index 00000000..e788cd96 Binary files /dev/null and b/app/data/disconnected.png differ diff --git a/app/meson.build b/app/meson.build index c7fb6d18..275035a2 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', diff --git a/app/src/disconnect.c b/app/src/disconnect.c new file mode 100644 index 00000000..4ea4d56c --- /dev/null +++ b/app/src/disconnect.c @@ -0,0 +1,89 @@ +#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"); + } + + if (d->deadline != SC_TICK_NONE) { + 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); + } + sc_mutex_unlock(&d->mutex); + + if (!d->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; + } + + ok = sc_thread_create(&d->thread, run, "scrcpy-dis", d); + if (!ok) { + goto error_destroy_cond; + } + + d->deadline = deadline; + d->interrupted = false; + + assert(cbs && cbs->on_icon_loaded && cbs->on_timeout); + d->cbs = cbs; + d->cbs_userdata = cbs_userdata; + + return true; + +error_destroy_mutex: + sc_mutex_destroy(&d->mutex); +error_destroy_cond: + sc_cond_destroy(&d->cond); + + 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 00000000..5b63ee59 --- /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.h b/app/src/events.h index 7e2ad38c..2a4a8965 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -13,12 +13,13 @@ 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 diff --git a/app/src/icon.h b/app/src/icon.h index 48817f37..44951cce 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -6,6 +6,7 @@ #include #define SC_ICON_FILENAME_SCRCPY "scrcpy.png" +#define SC_ICON_FILENAME_DISCONNECTED "disconnected.png" SDL_Surface * sc_icon_load(const char *filename); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index cd6f9a16..473aba2c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -167,23 +167,41 @@ 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, bool disconnected) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case SC_EVENT_DEVICE_DISCONNECTED: + if (disconnected) { + break; + } LOGW("Device disconnected"); + if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { + return SCRCPY_EXIT_FAILURE; + } return SCRCPY_EXIT_DISCONNECTED; case SC_EVENT_DEMUXER_ERROR: + if (disconnected) { + break; + } LOGE("Demuxer error"); return SCRCPY_EXIT_FAILURE; case SC_EVENT_CONTROLLER_ERROR: + if (disconnected) { + break; + } LOGE("Controller error"); return SCRCPY_EXIT_FAILURE; case SC_EVENT_RECORDER_ERROR: + if (disconnected) { + break; + } LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; case SC_EVENT_AOA_OPEN_ERROR: + if (disconnected) { + break; + } LOGE("AOA open error"); return SCRCPY_EXIT_FAILURE; case SC_EVENT_TIME_LIMIT_REACHED: @@ -192,6 +210,9 @@ event_loop(struct scrcpy *s, bool has_screen) { case SDL_EVENT_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_DISCONNECTED_TIMEOUT: + LOGD("Closing after device disconnection"); + return SCRCPY_EXIT_DISCONNECTED; case SC_EVENT_RUN_ON_MAIN_THREAD: { sc_runnable_fn run = event.user.data1; void *userdata = event.user.data2; @@ -414,6 +435,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; @@ -945,16 +967,9 @@ aoa_complete: } } - ret = event_loop(s, options->window); + ret = event_loop(s, options->window, false); 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); - } + disconnected = ret == SCRCPY_EXIT_DISCONNECTED; end: if (timeout_started) { @@ -999,6 +1014,25 @@ end: sc_server_stop(&s->server); } + if (screen_initialized && ret != SCRCPY_EXIT_DISCONNECTED) { + assert(options->window); + // 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 (screen_initialized && options->window) { + if (disconnected) { + ret = event_loop(s, options->window, true); + sc_screen_interrupt_disconnect(&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 c34fa921..34059f8d 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); @@ -210,7 +210,9 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { bool ok = false; SDL_Texture *texture = screen->tex.texture; if (!texture) { - LOGW("No texture to render"); + if (!screen->disconnected) { + LOGW("No texture to render"); + } goto end; } @@ -346,6 +348,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; @@ -603,9 +607,19 @@ sc_screen_interrupt(struct sc_screen *screen) { sc_fps_counter_interrupt(&screen->fps_counter); } +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 @@ -613,6 +627,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 @@ -852,12 +869,37 @@ 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 +} + bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { // !video implies !has_video_window assert(screen->video || !screen->has_video_window); switch (event->type) { case SC_EVENT_NEW_FRAME: { + if (screen->disconnected) { + // ignore + return true; + } bool ok = sc_screen_update_frame(screen); if (!ok) { LOGE("Frame update failed\n"); @@ -892,6 +934,44 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { sc_screen_render(screen, true); } return true; + case SC_EVENT_DEVICE_DISCONNECTED: + if (screen->disconnected) { + return true; + } + screen->disconnected = true; + 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; + } + + // else not fatal + return true; + 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); + return true; + } } if (sc_screen_is_relative_mode(screen) diff --git a/app/src/screen.h b/app/src/screen.h index 330e41a6..8bb932b0 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" @@ -77,6 +78,10 @@ struct sc_screen { bool paused; AVFrame *resume_frame; + + bool disconnected; + bool disconnect_started; + struct sc_disconnect disconnect; }; struct sc_screen_params { @@ -116,10 +121,15 @@ 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); +// request to interrupt the disconnected state (before closing the window) +// must be called before sc_screen_join(); +void +sc_screen_interrupt_disconnect(struct sc_screen *screen); + // join any inner thread void sc_screen_join(struct sc_screen *screen); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index c79aee74..198ea16a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -31,23 +31,33 @@ 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 -event_loop(struct scrcpy_otg *s) { +event_loop(struct scrcpy_otg *s, bool disconnected) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { - case SC_EVENT_USB_DEVICE_DISCONNECTED: + case SC_EVENT_DEVICE_DISCONNECTED: + if (disconnected) { + break; + } LOGW("Device disconnected"); + sc_screen_handle_event(&s->screen, &event); return SCRCPY_EXIT_DISCONNECTED; case SC_EVENT_AOA_OPEN_ERROR: + if (disconnected) { + break; + } LOGE("AOA open error"); return SCRCPY_EXIT_FAILURE; case SDL_EVENT_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_DISCONNECTED_TIMEOUT: + LOGD("Closing after device disconnection"); + return SCRCPY_EXIT_DISCONNECTED; default: sc_screen_handle_event(&s->screen, &event); break; @@ -96,6 +106,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 @@ -220,8 +231,8 @@ scrcpy_otg(struct scrcpy_options *options) { sc_usb_device_destroy(&usb_device); usb_device_initialized = false; - ret = event_loop(s); - LOGD("quit..."); + ret = event_loop(s, false); + disconnected = ret == SCRCPY_EXIT_DISCONNECTED; end: if (aoa_started) { @@ -231,6 +242,15 @@ end: if (screen_initialized) { sc_screen_interrupt(&s->screen); + + if (disconnected) { + ret = event_loop(s, true); + sc_screen_interrupt_disconnect(&s->screen); + } + LOGD("Quit..."); + + // Close the window immediately + sc_screen_hide_window(&s->screen); } if (mp) { diff --git a/app/src/util/tick.h b/app/src/util/tick.h index b037734b..646ada77 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -6,6 +6,7 @@ #include typedef int64_t sc_tick; +#define SC_TICK_NONE INT64_MIN #define PRItick PRIi64 #define SC_TICK_FREQ 1000000 // microsecond