Display disconnected icon before closing

Previously, when the connection to the device was lost while mirroring,
the window closed immediately, suggesting scrcpy had crashed.

To make it clear that a disconnection occurred, display a disconnected
icon for 2 seconds before closing the window.

PR #6662 <https://github.com/Genymobile/scrcpy/pull/6662>
This commit is contained in:
Romain Vimont 2026-02-24 00:26:22 +01:00
parent 1da65bbc98
commit 4c84937186
12 changed files with 335 additions and 24 deletions

BIN
app/data/disconnected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

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

88
app/src/disconnect.c Normal file
View File

@ -0,0 +1,88 @@
#include "disconnect.h"
#include <assert.h>
#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);
}

47
app/src/disconnect.h Normal file
View File

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

View File

@ -14,12 +14,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

View File

@ -6,6 +6,7 @@
#include <SDL3/SDL_surface.h>
#define SC_ICON_FILENAME_SCRCPY "scrcpy.png"
#define SC_ICON_FILENAME_DISCONNECTED "disconnected.png"
SDL_Surface *
sc_icon_load(const char *filename);

View File

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

View File

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

View File

@ -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");
@ -415,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;
@ -948,14 +952,7 @@ aoa_complete:
ret = event_loop(s, options->window);
terminate_runnables_on_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) {
@ -1000,6 +997,17 @@ end:
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);
}

View File

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

View File

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

View File

@ -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 @@ end:
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) {