Compare commits

..

8 Commits

Author SHA1 Message Date
Romain Vimont
678ecadff7 window_shown 2026-02-22 11:30:40 +01:00
Romain Vimont
59af4b4a03 Display disconnected icon before closing
When the connection to the device is 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.
2026-02-22 11:19:20 +01:00
Romain Vimont
2bdefc7663 Add utility to push an SDL event with data 2026-02-22 10:00:40 +01:00
Romain Vimont
9a079a716d Add function to delete current texture 2026-02-22 10:00:40 +01:00
Romain Vimont
cb6eee82cc Add filename parameter to icon loading
Replace scrcpy_icon_load(), which loaded the unique scrcpy app icon,
with sc_icon_load(filename), which can load any icon from the icons
directory.
2026-02-22 10:00:40 +01:00
Romain Vimont
ced45d0ec4 Replace SCRCPY_ICON_PATH with SCRCPY_ICON_DIR
SCRCPY_ICON_PATH defined the path of the scrcpy app icon.
SCRCPY_ICON_DIR defines the directory where scrcpy icons reside.

This change prepares for the addition of other icons.
2026-02-22 10:00:40 +01:00
Romain Vimont
7958b297ec Extract function to build file paths
Add a utility function to create a full path from a directory and a
filename.
2026-02-22 10:00:40 +01:00
Romain Vimont
b4711a5904 Rename icon.png to scrcpy.png
This makes the icon name consistent everywhere.
2026-02-22 10:00:40 +01:00
11 changed files with 93 additions and 144 deletions

View File

@@ -1729,7 +1729,7 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return true;
}
LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, "
"flip90, flip180 or flip270)", s);
"flip90, flip180 or flip270)", optarg);
return false;
}

View File

@@ -16,16 +16,17 @@ run(void *userdata) {
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 (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 (!interrupted) {
d->cbs->on_timeout(d, d->cbs_userdata);
if (!d->interrupted) {
d->cbs->on_timeout(d, d->cbs_userdata);
}
}
return 0;
@@ -45,6 +46,11 @@ sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline,
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;
@@ -52,17 +58,12 @@ sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline,
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);
error_destroy_cond:
sc_cond_destroy(&d->cond);
return false;
}

View File

@@ -6,7 +6,7 @@
#include "util/thread.h"
bool
sc_push_event_impl(uint32_t type, void *ptr, const char *name) {
sc_push_event_impl(uint32_t type, void* ptr, const char *name) {
SDL_Event event = {
.user = {
.type = type,

View File

@@ -9,7 +9,6 @@
enum {
SC_EVENT_NEW_FRAME = SDL_EVENT_USER,
SC_EVENT_OPEN_WINDOW,
SC_EVENT_RUN_ON_MAIN_THREAD,
SC_EVENT_DEVICE_DISCONNECTED,
SC_EVENT_SERVER_CONNECTION_FAILED,
@@ -24,7 +23,7 @@ enum {
};
bool
sc_push_event_impl(uint32_t type, void *ptr, 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, NULL, # TYPE)
#define sc_push_event_with_data(TYPE, PTR) sc_push_event_impl(TYPE, PTR, # TYPE)

View File

@@ -7,7 +7,6 @@
#include "android/input.h"
#include "android/keycodes.h"
#include "events.h"
#include "input_events.h"
#include "screen.h"
#include "shortcut_mod.h"
@@ -47,8 +46,6 @@ 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
@@ -352,7 +349,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 || im->disconnected) {
if (im->camera || !im->kp || im->screen->paused) {
return;
}
@@ -420,7 +417,6 @@ 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;
@@ -437,7 +433,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 && !disconnected) {
if (down && !repeat) {
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
++im->key_repeat;
} else {
@@ -514,12 +510,6 @@ 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
@@ -728,7 +718,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 || im->disconnected) {
if (im->camera || !im->mp || im->screen->paused) {
return;
}
@@ -767,7 +757,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 || im->disconnected) {
if (im->camera || !im->mp || im->screen->paused) {
return;
}
@@ -822,7 +812,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 || im->disconnected) {
if (im->camera) {
return;
}
@@ -993,7 +983,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 || im->disconnected) {
if (im->camera || !im->kp || im->screen->paused) {
return;
}
@@ -1023,7 +1013,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 || im->disconnected) {
if (im->camera || !im->gp) {
return;
}
@@ -1068,7 +1058,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 || im->disconnected) {
if (im->camera || !im->gp || im->screen->paused) {
return;
}
@@ -1088,7 +1078,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 || im->disconnected) {
if (im->camera || !im->gp || im->screen->paused) {
return;
}
@@ -1114,7 +1104,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 || im->disconnected) {
if (im->camera || !im->controller) {
return;
}
@@ -1137,16 +1127,6 @@ 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) {
@@ -1187,8 +1167,5 @@ 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,8 +46,6 @@ 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

@@ -212,18 +212,17 @@ event_loop(struct scrcpy *s, bool has_screen) {
}
static void
terminate_runnables_on_event_loop(void) {
terminate_event_loop(void) {
sc_reject_new_runnables();
SDL_Event event;
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);
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);
}
}
}
@@ -951,7 +950,7 @@ aoa_complete:
}
ret = event_loop(s, options->window);
terminate_runnables_on_event_loop();
terminate_event_loop();
disconnected = ret == SCRCPY_EXIT_DISCONNECTED;
end:

View File

@@ -198,30 +198,21 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->window_shown);
assert(!screen->video || screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
SDL_Renderer *renderer = screen->renderer;
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
sc_sdl_render_clear(renderer);
bool ok = false;
SDL_Texture *texture = screen->tex.texture;
if (!texture) {
// Draw a dark 10x10 square in the top-right corner to distinguish a
// black frame from the absence of a frame
struct sc_size render_size = sc_sdl_get_render_output_size(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0x33, 0xff);
SDL_FRect rect = {
.x = render_size.width - 20,
.y = 10,
.w = 10,
.h = 10,
};
SDL_RenderFillRect(renderer, &rect);
if (!screen->disconnected) {
LOGW("No texture to render");
}
goto end;
}
@@ -291,8 +282,11 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
if (ctx->width <= 0 || ctx->width > 0xFFFF
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
@@ -300,19 +294,6 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
return false;
}
// content_size can be written from this thread, because it is never read
// from the main thread before handling SC_EVENT_OPEN_WINDOW (which acts as
// a synchronization point) when video is enabled
screen->frame_size.width = session->video.width;
screen->frame_size.height = session->video.height;
screen->content_size = get_oriented_size(screen->frame_size,
screen->orientation);
bool ok = sc_push_event(SC_EVENT_OPEN_WINDOW);
if (!ok) {
return false;
}
#ifndef NDEBUG
screen->open = true;
#endif
@@ -362,7 +343,9 @@ bool
sc_screen_init(struct sc_screen *screen,
const struct sc_screen_params *params) {
screen->resize_pending = false;
screen->window_shown = false;
screen->has_frame = false;
screen->has_video_window = false;
screen->window_shown = true;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@@ -555,8 +538,8 @@ sc_screen_init(struct sc_screen *screen,
if (!screen->video) {
// Show the window immediately
screen->window_shown = true;
sc_sdl_show_window(screen->window);
screen->window_shown = true;
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
@@ -612,15 +595,15 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
sc_fps_counter_start(&screen->fps_counter);
}
screen->window_shown = true;
sc_sdl_show_window(screen->window);
screen->window_shown = true;
sc_screen_update_content_rect(screen);
}
void
sc_screen_hide_window(struct sc_screen *screen) {
sc_sdl_hide_window(screen->window);
screen->window_shown = false;
sc_sdl_hide_window(screen->window);
}
void
@@ -660,17 +643,6 @@ 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
@@ -741,14 +713,14 @@ sc_screen_set_orientation(struct sc_screen *screen,
static bool
sc_screen_apply_frame(struct sc_screen *screen) {
assert(screen->video);
assert(screen->window_shown);
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
AVFrame *frame = screen->frame;
struct sc_size new_frame_size = {frame->width, frame->height};
if (screen->frame_size.width != new_frame_size.width
if (!screen->has_frame
|| screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
@@ -756,8 +728,14 @@ sc_screen_apply_frame(struct sc_screen *screen) {
struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation);
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
if (screen->has_frame) {
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
} else {
// This is the first frame
screen->has_frame = true;
screen->content_size = new_content_size;
}
}
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
@@ -765,6 +743,18 @@ sc_screen_apply_frame(struct sc_screen *screen) {
return false;
}
assert(screen->has_frame);
if (!screen->has_video_window) {
screen->has_video_window = true;
// this is the very first frame, show the window
sc_screen_show_initial_window(screen);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
}
sc_screen_render(screen, false);
return true;
}
@@ -906,17 +896,9 @@ sc_disconnect_on_timeout(struct sc_disconnect *d, void *userdata) {
void
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_OPEN_WINDOW:
sc_screen_show_initial_window(screen);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
sc_screen_render(screen, false);
return;
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
@@ -925,27 +907,28 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
return;
}
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_render(screen, true);
if (!screen->video || screen->has_video_window) {
sc_screen_render(screen, true);
}
return;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
// This event can be triggered before the window is shown
if (screen->window_shown) {
if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return;
case SDL_EVENT_WINDOW_RESTORED:
if (screen->video && is_windowed(screen)) {
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->video);
assert(screen->has_video_window);
return;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->video);
assert(screen->has_video_window);
if (is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
@@ -959,8 +942,6 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
return;
}
sc_input_manager_handle_event(&screen->im, event);
sc_texture_reset(&screen->tex);
sc_screen_render(screen, true);
@@ -994,23 +975,14 @@ sc_screen_handle_disconnection(struct sc_screen *screen) {
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);
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;
@@ -1025,15 +997,15 @@ sc_screen_handle_disconnection(struct sc_screen *screen) {
}
case SC_EVENT_DISCONNECTED_TIMEOUT:
LOGD("Closing after device disconnection");
return;
goto end;
case SDL_EVENT_QUIT:
LOGD("User requested to quit");
sc_screen_interrupt_disconnect(screen);
return;
default:
sc_input_manager_handle_event(&screen->im, &event);
goto end;
}
}
end:
sc_screen_interrupt_disconnect(screen);
}
struct sc_point

View File

@@ -71,6 +71,8 @@ struct sc_screen {
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_FRect rect;
bool has_frame;
bool has_video_window;
bool window_shown;
AVFrame *frame;

View File

@@ -6,6 +6,7 @@
#include <stdint.h>
typedef int64_t sc_tick;
#define SC_TICK_NONE INT64_MIN
#define PRItick PRIi64
#define SC_TICK_FREQ 1000000 // microsecond

View File

@@ -93,4 +93,4 @@ Then just double-click on that file to run it.
To start scrcpy without opening a terminal, double-click `scrcpy-noconsole.vbs`
(note that errors won't be shown). To pass arguments, edit (a copy of)
`scrcpy-noconsole.vbs` and add the desired arguments.
`scrcpy-noconsole.vbs` add and the desired arguments.