From 4c9262664c39f67682b876eb4331e9f2b8a7c44e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 10 Apr 2026 12:41:22 +0200 Subject: [PATCH] wayland: implement session management If the compositor supports session management, the mpv window is always added to a session. The user can manage multiple sessions by using `--wayland-session=`. Human-readable names are mapped to session ids via files under `~/.local/state/mpv/sessions`. The file name is computed as a hash of XDG_CURRENT_DESKTOP and the human-readable name. If multiple mpv instances are started with the same session name, the latter mpv instance takes over the session. Upon session restoration, the mpv window is restored by the compositor according to its state from the previous session. What that entails is compositor policy. --- DOCS/interface-changes/wayland-session.txt | 2 + DOCS/man/options.rst | 5 + options/options.c | 1 + options/options.h | 1 + video/out/meson.build | 6 +- video/out/wayland_common.c | 112 +++++++++++++++++++++ video/out/wayland_common.h | 5 + 7 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 DOCS/interface-changes/wayland-session.txt diff --git a/DOCS/interface-changes/wayland-session.txt b/DOCS/interface-changes/wayland-session.txt new file mode 100644 index 0000000000..ea74ec1168 --- /dev/null +++ b/DOCS/interface-changes/wayland-session.txt @@ -0,0 +1,2 @@ +add `--wayland-session=` +restore the mpv window on wayland using xdg-session-management-v1 diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 5a9bdc64f3..62c5b6afbc 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -6351,6 +6351,11 @@ them. frame presentation if it is supported by the compositor (default: ``yes``). This only has an effect if ``--video-sync=display-...`` is being used. +``--wayland-session=`` + Set the wayland session name for window restoration (default: unset). + Not setting this or setting it to the empty string disables session + management. + ``--spirv-compiler=`` Controls which compiler is used to translate GLSL to SPIR-V. This is only relevant for ``--gpu-api=d3d11`` with ``--vo=gpu``. diff --git a/options/options.c b/options/options.c index 618b9e6916..9f3edebdd1 100644 --- a/options/options.c +++ b/options/options.c @@ -220,6 +220,7 @@ static const m_option_t mp_vo_opt_list[] = { {"wayland-edge-pixels-touch", OPT_INT(wl_edge_pixels_touch), M_RANGE(0, INT_MAX)}, {"wayland-present", OPT_BOOL(wl_present)}, + {"wayland-session", OPT_STRING(wayland_session)}, #endif #if HAVE_WIN32_DESKTOP // For old MinGW-w64 compatibility diff --git a/options/options.h b/options/options.h index a8f82a9810..82717baf51 100644 --- a/options/options.h +++ b/options/options.h @@ -28,6 +28,7 @@ typedef struct mp_vo_opts { char *fsscreen_name; char *winname; char *appid; + char *wayland_session; int x11_netwm; int x11_bypass_compositor; int x11_present; diff --git a/video/out/meson.build b/video/out/meson.build index 9264e505c3..722627c825 100644 --- a/video/out/meson.build +++ b/video/out/meson.build @@ -17,7 +17,7 @@ protocols = [[wl_protocol_dir, 'stable/linux-dmabuf/linux-dmabuf-v1.xml'], wl_protocols_source = [] wl_protocols_headers = [] -foreach v: ['1.39', '1.41', '1.44', '1.47'] +foreach v: ['1.39', '1.41', '1.44', '1.47', '1.48'] features += {'wayland-protocols-' + v.replace('.', '-'): wayland['deps'][2].version().version_compare('>=' + v)} endforeach @@ -34,6 +34,10 @@ if features['wayland-protocols-1-44'] protocols += [[wl_protocol_dir, 'staging/color-representation/color-representation-v1.xml']] endif +if features['wayland-protocols-1-48'] + protocols += [[wl_protocol_dir, 'staging/xdg-session-management/xdg-session-management-v1.xml']] +endif + foreach p: protocols xml = join_paths(p) wl_protocols_source += custom_target(xml.underscorify() + '_c', diff --git a/video/out/wayland_common.c b/video/out/wayland_common.c index 6862eaa644..5a2443f64c 100644 --- a/video/out/wayland_common.c +++ b/video/out/wayland_common.c @@ -27,11 +27,16 @@ #include "common/msg.h" #include "input/input.h" #include "input/keycodes.h" +#include "misc/hash.h" +#include "misc/io_utils.h" +#include "misc/path_utils.h" #include "options/m_config.h" +#include "options/path.h" #include "osdep/io.h" #include "osdep/poll_wrapper.h" #include "osdep/timer.h" #include "present_sync.h" +#include "stream/stream.h" #include "video/out/gpu/video.h" #include "wayland_common.h" #include "win_state.h" @@ -60,6 +65,10 @@ #include "color-representation-v1.h" #endif +#if HAVE_WAYLAND_PROTOCOLS_1_48 +#include "xdg-session-management-v1.h" +#endif + #ifndef CLOCK_MONOTONIC_RAW #define CLOCK_MONOTONIC_RAW 4 #endif @@ -331,6 +340,10 @@ static void set_surface_scaling(struct vo_wayland_state *wl); static void update_output_scaling(struct vo_wayland_state *wl); static void update_output_geometry(struct vo_wayland_state *wl); static void destroy_offer(struct vo_wayland_data_offer *o); +#if HAVE_WAYLAND_PROTOCOLS_1_48 +static char *session_file(void *talloc_ctx, const char *session, struct vo *vo); +static char *read_session_id(void *talloc_ctx, struct vo_wayland_state *wl, const char *path); +#endif /* Wayland listener boilerplate */ static void pointer_handle_enter(void *data, struct wl_pointer *pointer, @@ -2717,6 +2730,31 @@ static const struct zwp_linux_dmabuf_feedback_v1_listener dmabuf_feedback_listen .tranche_flags = tranche_flags, }; +#if HAVE_WAYLAND_PROTOCOLS_1_48 +static void xdg_session_created(void *data, struct xdg_session_v1 *xdg_session_v1, const char *session_id) +{ + struct vo_wayland_state *wl = data; + mp_save_to_file(wl->session_file, session_id, strlen(session_id)); +} + +static void xdg_session_restored(void *data, struct xdg_session_v1 *xdg_session_v1) +{ + // nothing +} + +static void xdg_session_replaced(void *data, struct xdg_session_v1 *xdg_session_v1) +{ + struct vo_wayland_state *wl = data; + MP_WARN(wl, "Session has been replaced!\n"); +} + +static const struct xdg_session_v1_listener xdg_session_listener = { + .created = xdg_session_created, + .restored = xdg_session_restored, + .replaced = xdg_session_replaced, +}; +#endif + static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id, const char *interface, uint32_t ver) { @@ -2895,6 +2933,25 @@ static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id wl->wp_tablet_manager = wl_registry_bind(reg, id, &zwp_tablet_manager_v2_interface, ver); } +#if HAVE_WAYLAND_PROTOCOLS_1_48 + if (wl->session_file && + !strcmp(interface, xdg_session_manager_v1_interface.name) && + found++) + { + ver = 1; + struct xdg_session_manager_v1 *xdg_session_manager = + wl_registry_bind(reg, id, &xdg_session_manager_v1_interface, ver); + void *tmp = talloc_new(NULL); + char *session_id = read_session_id(tmp, wl, wl->session_file); + wl->xdg_session = xdg_session_manager_v1_get_session(xdg_session_manager, + XDG_SESSION_MANAGER_V1_REASON_LAUNCH, + session_id); + xdg_session_v1_add_listener(wl->xdg_session, &xdg_session_listener, wl); + talloc_free(tmp); + xdg_session_manager_v1_destroy(xdg_session_manager); + } +#endif + if (found > 1) MP_VERBOSE(wl, "Registered interface %s at version %d\n", interface, ver); } @@ -3167,6 +3224,14 @@ static int create_xdg_surface(struct vo_wayland_state *wl) MP_ERR(wl, "failed to create xdg_surface and xdg_toplevel!\n"); return 1; } + +#if HAVE_WAYLAND_PROTOCOLS_1_48 + if (wl->xdg_session) { + wl->xdg_toplevel_session = + xdg_session_v1_restore_toplevel(wl->xdg_session, wl->xdg_toplevel, "mpv"); + } +#endif + return 0; } @@ -4450,6 +4515,11 @@ bool vo_wayland_init(struct vo *vo) if (create_input(wl)) goto err; +#if HAVE_WAYLAND_PROTOCOLS_1_48 + if (wl->opts->wayland_session && *wl->opts->wayland_session) + wl->session_file = session_file(wl, wl->opts->wayland_session, vo); +#endif + wl->registry = wl_display_get_registry(wl->display); wl_registry_add_listener(wl->registry, ®istry_listener, wl); @@ -4852,6 +4922,14 @@ void vo_wayland_uninit(struct vo *vo) if (wl->wp_tablet_manager) zwp_tablet_manager_v2_destroy(wl->wp_tablet_manager); +#if HAVE_WAYLAND_PROTOCOLS_1_48 + if (wl->xdg_toplevel_session) + xdg_toplevel_session_v1_destroy(wl->xdg_toplevel_session); + + if (wl->xdg_session) + xdg_session_v1_destroy(wl->xdg_session); +#endif + if (wl->display) wl_display_disconnect(wl->display); @@ -4933,3 +5011,37 @@ void vo_wayland_wakeup(struct vo *vo) struct vo_wayland_state *wl = vo->wl; (void)write(wl->wakeup_pipe[1], &(char){0}, 1); } + +#if HAVE_WAYLAND_PROTOCOLS_1_48 +static char *session_file(void *talloc_ctx, const char *session, struct vo *vo) +{ + void *tmp = talloc_new(NULL); + char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); + if (!xdg_current_desktop) + xdg_current_desktop = ""; + bstr full_name = {0}; + bstr_xappend_asprintf(tmp, &full_name, "%16" PRIx64 "%s%16" PRIx64 "%s", + (uint64_t)strlen(xdg_current_desktop), + xdg_current_desktop, (uint64_t)strlen(session), + session); + bstr file_name = mp_hash_to_bstr(tmp, full_name.start, full_name.len, + "SHA256"); + char *file_path = mp_find_user_file(tmp, vo->global, "state", "sessions"); + mp_mkdirp(file_path); + file_path = mp_path_join_bstr(talloc_ctx, bstr0(file_path), file_name); + talloc_free(tmp); + return file_path; +} + +static char *read_session_id(void *talloc_ctx, struct vo_wayland_state *wl, const char *path) +{ + if (!mp_path_exists(path)) + return NULL; + bstr id = stream_read_file(path, talloc_ctx, wl->vo->global, 4096); + if (!id.len) + return NULL; + if (bstr_validate_utf8(id) < 0) + return NULL; + return bstrto0(talloc_ctx, id); +} +#endif diff --git a/video/out/wayland_common.h b/video/out/wayland_common.h index 804dcf9afa..f3f40fb013 100644 --- a/video/out/wayland_common.h +++ b/video/out/wayland_common.h @@ -198,6 +198,11 @@ struct vo_wayland_state { bool cursor_visible; int allocated_cursor_scale; struct vo_wayland_seat *last_button_seat; + + /* Session Management */ + char *session_file; + struct xdg_session_v1 *xdg_session; + struct xdg_toplevel_session_v1 *xdg_toplevel_session; }; bool vo_wayland_check_visible(struct vo *vo);