diff --git a/CMakeLists.txt b/CMakeLists.txt index 63e261c..a03ce77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ project(wl-mirror C) option(INSTALL_EXAMPLE_SCRIPTS "install wl-mirror example scripts" OFF) option(INSTALL_DOCUMENTATION "install wl-mirror manual pages" OFF) option(WITH_LIBDECOR "use libdecor for window decoration" OFF) +option(WITH_XDG_PORTAL_BACKEND "enable the xdg-desktop-portal / pipewire backend" OFF) +set(SD_BUS_PROVIDER AUTO CACHE STRING "provider library for sd-bus") set(FORCE_WAYLAND_SCANNER_PATH "" CACHE STRING "provide a custom path for wayland-scanner") # wayland protocols needed by wl-mirror @@ -19,6 +21,7 @@ set(PROTOCOLS "stable/viewporter/viewporter.xml" "staging/fractional-scale/fractional-scale-v1.xml" "unstable/xdg-output/xdg-output-unstable-v1.xml" + "unstable/xdg-foreign/xdg-foreign-unstable-v2.xml" "unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml" "unstable/wlr-export-dmabuf-unstable-v1.xml" "unstable/wlr-screencopy-unstable-v1.xml" diff --git a/README.md b/README.md index 8baef68..209c192 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ backends: - auto automatically try the backends in order and use the first that works (default) - dmabuf use the wlr-export-dmabuf-unstable-v1 protocol to capture outputs - screencopy use the wlr-screencopy-unstable-v1 protocol to capture outputs + - xdg-portal use xdg-desktop-portal and pipewire to capture outputs or windows (WIP) + - pipewire alias for 'xdg-portal' (WIP) transforms: transforms are specified as a dash-separated list of flips followed by a rotation @@ -172,6 +174,8 @@ on (see issues [#16](https://github.com/Ferdi265/wl-mirror/issues/16) and - `libGLESv2` - `epoll-shim` (on systems that do not have `epoll`, e.g. FreeBSD) - `libdecor` (see `WITH_LIBDECOR`) +- `libsystemd` or `libelogind` or `libbasu` (for xdg-desktop-portal backend, see `WITH_XDG_PORTAL_BACKEND`) +- `libpipewire-0.3` (for xdg-desktop-portal backend, see `WITH_XDG_PORTAL_BACKEND`) - `wayland-scanner` - `scdoc` (for manual pages, see `INSTALL_DOCUMENTATION`) @@ -193,9 +197,12 @@ on (see issues [#16](https://github.com/Ferdi265/wl-mirror/issues/16) and - `INSTALL_EXAMPLE_SCRIPTS`: also install example scripts (default `OFF`) - `INSTALL_DOCUMENTATION`: also build and install manual pages (default `OFF`) - `WITH_LIBDECOR`: build with libdecor for window decoration (default `OFF`) +- `WITH_XDG_PORTAL_BACKEND`: enable the xdg-desktop-portal and pipewire screen capture backend (default `OFF`) +- `SD_BUS_PROVIDER`: the library used to provide sd-bus (default `AUTO`) - `FORCE_WAYLAND_SCANNER_PATH`: always use the provided path for wayland-scanner, do not use pkg-config (default empty) - `FORCE_SYSTEM_WL_PROTOCOLS`: always use system-installed wayland-protocols, do not use submodules (default `OFF`) - `FORCE_SYSTEM_WLR_PROTOCOLS`: always use system-installed wlr-protocols, do not use submodules (default `OFF`) +- `WITH_XDG_PORTAL_BACKEND`: enable the xdg-desktop-portal and pipewire screen capture backend (default `OFF`) - `WL_PROTOCOL_DIR`: directory where system-installed wayland-protocols are located (default `/usr/share/wayland-protocols`) - `WLR_PROTOCOL_DIR`: directory where system-installed wlr-protocols are located (default `/usr/share/wlr-protocols`) @@ -208,6 +215,7 @@ on (see issues [#16](https://github.com/Ferdi265/wl-mirror/issues/16) and - `src/mirror.c`: output mirroring code - `src/mirror-dmabuf.c`: wlr-export-dmabuf-unstable-v1 backend code - `src/mirror-screencopy.c`: wlr-screencopy-unstable-v1 backend code +- `src/mirror-xdg-portal.c`: xdg-desktop-portal and pipewire backend code (WIP) - `src/transform.c`: matrix transformation code - `src/event.c`: event loop - `src/stream.c`: asynchronous option stream input diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index ccdfa17..30a5fce 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -59,3 +59,15 @@ if(${WITH_LIBDECOR}) target_link_libraries(deps INTERFACE PkgConfig::LibDecor) target_compile_definitions(deps INTERFACE WITH_LIBDECOR) endif() + +# find xdg-portal dependencies (dbus and pipewire) +if(${WITH_XDG_PORTAL_BACKEND}) + if(${SD_BUS_PROVIDER} STREQUAL AUTO) + do_pkg_search_module(SDBus REQUIRED IMPORTED_TARGET "libsystemd" "libelogind" "basu") + else() + pkg_check_modules(SDBus REQUIRED IMPORTED_TARGET "${SD_BUS_PROVIDER}") + endif() + pkg_check_modules(PipeWire REQUIRED IMPORTED_TARGET "libpipewire-0.3") + target_link_libraries(deps INTERFACE PkgConfig::SDBus PkgConfig::PipeWire) + target_compile_definitions(deps INTERFACE WITH_XDG_PORTAL_BACKEND) +endif() diff --git a/include/wlm/mirror-backends.h b/include/wlm/mirror-backends.h index e5496fc..1bba2fb 100644 --- a/include/wlm/mirror-backends.h +++ b/include/wlm/mirror-backends.h @@ -15,5 +15,6 @@ typedef struct mirror_backend { void wlm_mirror_dmabuf_init(struct ctx * ctx); void wlm_mirror_screencopy_init(struct ctx * ctx); +void wlm_mirror_xdg_portal_init(struct ctx * ctx); #endif diff --git a/include/wlm/mirror-xdg-portal.h b/include/wlm/mirror-xdg-portal.h new file mode 100644 index 0000000..f386747 --- /dev/null +++ b/include/wlm/mirror-xdg-portal.h @@ -0,0 +1,93 @@ +#ifndef WL_MIRROR_MIRROR_XDG_PORTAL_H_ +#define WL_MIRROR_MIRROR_XDG_PORTAL_H_ + +#include +#include +#include + +#include +#include + +struct xdg_portal_mirror_backend; + +typedef enum { + SCREENCAST_MONITOR = 1, + SCREENCAST_WINDOW = 2, + SCREENCAST_VIRTUAL = 4 +} screencast_source_types_t; + +typedef enum { + SCREENCAST_HIDDEN = 1, + SCREENCAST_EMBEDDED = 2, + SCREENCAST_METADATA = 4 +} screencast_cursor_modes_t; + +typedef struct { + screencast_source_types_t source_types; + screencast_cursor_modes_t cursor_modes; + uint32_t version; +} screencast_properties_t; + +typedef int (*request_ctx_handler_t)(struct ctx * ctx, struct xdg_portal_mirror_backend * backend, sd_bus_message * reply); + +typedef struct { + struct ctx * ctx; + const char * name; + request_ctx_handler_t handler; +} request_ctx_t; + +#define SCREENCAST_MIN_VERSION 2 + +typedef enum { + STATE_IDLE, + STATE_GET_PROPERTIES, + STATE_CREATE_SESSION, + STATE_SELECT_SOURCES, + STATE_START, + STATE_OPEN_PIPEWIRE_REMOTE, + STATE_PW_INIT, + STATE_PW_CREATE_STREAM, + STATE_RUNNING, + STATE_BROKEN +} xdg_portal_state_t; + +typedef struct xdg_portal_mirror_backend { + mirror_backend_t header; + + // general info + uint32_t x, y, w, h; + uint32_t gl_format; + uint32_t drm_format; + uint64_t drm_modifier; + + // sd-bus state + screencast_properties_t screencast_properties; + char * request_handle; + char * session_handle; + bool session_open; + + request_ctx_t rctx; + sd_bus_slot * call_slot; + sd_bus_slot * session_slot; + sd_bus * bus; + event_handler_t dbus_event_handler; + + // pipewire state + int pw_fd; + uint32_t pw_node_id; + uint32_t pw_major; + uint32_t pw_minor; + uint32_t pw_patch; + + struct pw_loop * pw_loop; + struct pw_context * pw_context; + struct pw_core * pw_core; + struct pw_stream * pw_stream; + struct spa_hook pw_core_listener; + struct spa_hook pw_stream_listener; + event_handler_t pw_event_handler; + + xdg_portal_state_t state; +} xdg_portal_mirror_backend_t; + +#endif diff --git a/include/wlm/options.h b/include/wlm/options.h index 6959ac1..5ecf3ca 100644 --- a/include/wlm/options.h +++ b/include/wlm/options.h @@ -22,7 +22,10 @@ typedef enum { typedef enum { BACKEND_AUTO, BACKEND_DMABUF, - BACKEND_SCREENCOPY + BACKEND_SCREENCOPY, +#ifdef WITH_XDG_PORTAL_BACKEND + BACKEND_XDG_PORTAL, +#endif } backend_t; typedef struct ctx_opt { diff --git a/include/wlm/wayland.h b/include/wlm/wayland.h index 1c88f6b..e77dc02 100644 --- a/include/wlm/wayland.h +++ b/include/wlm/wayland.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -66,6 +67,12 @@ typedef struct ctx_wl { uint32_t shm_id; uint32_t screencopy_manager_id; + // xdg portal backend objects + struct zxdg_exporter_v2 * xdg_exporter; + struct zxdg_exported_v2 * xdg_exported_surface; + const char * xdg_exported_handle; + uint32_t xdg_exporter_id; + // output list output_list_node_t * outputs; seat_list_node_t * seats; diff --git a/src/egl.c b/src/egl.c index 7f019b5..d42a8c8 100644 --- a/src/egl.c +++ b/src/egl.c @@ -355,6 +355,7 @@ void wlm_egl_resize_viewport(ctx_t * ctx) { wlm_log_debug(ctx, "egl::resize_viewport(): view_width = %d, view_height = %d\n", view_width, view_height); // updating GL viewport + wlm_log_debug(ctx, "egl::resize_viewport(): win = %dx%d, view = %dx%d, tex = %dx%d\n", win_width, win_height, view_width, view_height, tex_width, tex_height); wlm_log_debug(ctx, "egl::resize_viewport(): viewport %d, %d, %d, %d\n", (int32_t)(win_width - view_width) / 2, (int32_t)(win_height - view_height) / 2, view_width, view_height ); @@ -504,6 +505,13 @@ bool wlm_egl_dmabuf_to_texture(ctx_t * ctx, dmabuf_t * dmabuf) { image_attribs[i++] = dmabuf->height; image_attribs[i++] = EGL_LINUX_DRM_FOURCC_EXT; image_attribs[i++] = dmabuf->drm_format; + wlm_log_debug(ctx, "egl::dmabuf_to_texture(): w=%d h=%d drm_format=%c%c%c%c\n", + dmabuf->width, dmabuf->height, + ((dmabuf->drm_format >> 0) & 0xFF), + ((dmabuf->drm_format >> 8) & 0xFF), + ((dmabuf->drm_format >> 16) & 0xFF), + ((dmabuf->drm_format >> 24) & 0xFF) + ); for (size_t j = 0; j < dmabuf->planes; j++) { image_attribs[i++] = fd_attribs[j]; @@ -516,10 +524,20 @@ bool wlm_egl_dmabuf_to_texture(ctx_t * ctx, dmabuf_t * dmabuf) { image_attribs[i++] = (uint32_t)dmabuf->modifier; image_attribs[i++] = modifier_high_attribs[j]; image_attribs[i++] = (uint32_t)(dmabuf->modifier >> 32); + wlm_log_debug(ctx, "egl::dmabuf_to_texture(): fd=% 3d offset=% 10d stride=% 10d modifier=%016lx\n", + dmabuf->fds[j], dmabuf->offsets[j], dmabuf->strides[j], dmabuf->modifier + ); } image_attribs[i++] = EGL_NONE; + wlm_log_debug(ctx, "egl::dmabuf_to_texture(): image_attribs="); + if (ctx->opt.verbose) { + for (i = 0; image_attribs[i] != EGL_NONE; i++) { + fprintf(stderr, "%08lx%s", image_attribs[i], image_attribs[i + 1] != EGL_NONE ? "_" : "\n"); + } + } + // create EGLImage from dmabuf with attribute array EGLImage frame_image = eglCreateImage(ctx->egl.display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, image_attribs); free(image_attribs); @@ -532,10 +550,18 @@ bool wlm_egl_dmabuf_to_texture(ctx_t * ctx, dmabuf_t * dmabuf) { // convert EGLImage to GL texture glBindTexture(GL_TEXTURE_2D, ctx->egl.texture); ctx->egl.glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, frame_image); + ctx->egl.texture_initialized = true; // destroy temporary image eglDestroyImage(ctx->egl.display, frame_image); + // set texture size and aspect ratio only if changed + if (dmabuf->width != ctx->egl.width || dmabuf->height != ctx->egl.height) { + ctx->egl.width = dmabuf->width; + ctx->egl.height = dmabuf->height; + wlm_egl_resize_viewport(ctx); + } + return true; } diff --git a/src/mirror-dmabuf.c b/src/mirror-dmabuf.c index a69df18..478e419 100644 --- a/src/mirror-dmabuf.c +++ b/src/mirror-dmabuf.c @@ -103,8 +103,13 @@ static void on_frame( backend->dmabuf.drm_format = format; backend->dmabuf.planes = num_objects; - wlm_log_debug(ctx, "mirror-dmabuf::on_frame(): w=%d h=%d gl_format=%x drm_format=%08x drm_modifier=%016lx\n", - backend->dmabuf.width, backend->dmabuf.height, GL_RGB8_OES, backend->dmabuf.drm_format, backend->dmabuf.modifier + wlm_log_debug(ctx, "mirror-dmabuf::on_frame(): w=%d h=%d gl_format=%x drm_format=%c%c%c%c drm_modifier=%016lx\n", + backend->dmabuf.width, backend->dmabuf.height, GL_RGB8_OES, + (backend->dmabuf.drm_format >> 0) & 0xFF, + (backend->dmabuf.drm_format >> 8) & 0xFF, + (backend->dmabuf.drm_format >> 16) & 0xFF, + (backend->dmabuf.drm_format >> 24) & 0xFF, + backend->dmabuf.modifier ); for (size_t i = 0; i < num_objects; i++) { @@ -177,7 +182,6 @@ static void on_ready( ctx->egl.format = GL_RGB8_OES; // FIXME: find out actual format ctx->egl.texture_region_aware = false; - ctx->egl.texture_initialized = true; // set buffer flags only if changed bool invert_y = backend->buffer_flags & ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT; @@ -186,13 +190,6 @@ static void on_ready( wlm_egl_update_uniforms(ctx); } - // set texture size and aspect ratio only if changed - if (backend->dmabuf.width != ctx->egl.width || backend->dmabuf.height != ctx->egl.height) { - ctx->egl.width = backend->dmabuf.width; - ctx->egl.height = backend->dmabuf.height; - wlm_egl_resize_viewport(ctx); - } - dmabuf_frame_cleanup(backend); backend->state = STATE_READY; backend->header.fail_count = 0; diff --git a/src/mirror-screencopy.c b/src/mirror-screencopy.c index 6dbc9e1..ba693b1 100644 --- a/src/mirror-screencopy.c +++ b/src/mirror-screencopy.c @@ -292,10 +292,10 @@ static void on_ready( wlm_log_debug(ctx, "mirror-screencopy::on_ready(): received ready event with width: %d, height: %d, stride: %d, format: %c%c%c%c\n", backend->frame_width, backend->frame_height, backend->frame_stride, - (backend->frame_format >> 24) & 0xff, - (backend->frame_format >> 16) & 0xff, + (backend->frame_format >> 0) & 0xff, (backend->frame_format >> 8) & 0xff, - (backend->frame_format >> 0) & 0xff + (backend->frame_format >> 16) & 0xff, + (backend->frame_format >> 24) & 0xff ); } diff --git a/src/mirror-xdg-portal.c b/src/mirror-xdg-portal.c new file mode 100644 index 0000000..ab320fa --- /dev/null +++ b/src/mirror-xdg-portal.c @@ -0,0 +1,1431 @@ +#ifdef WITH_XDG_PORTAL_BACKEND +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void wlm_mirror_backend_fail_async(xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_BROKEN; +} + +typedef struct { + uint32_t spa_format; + uint32_t drm_format; + GLint gl_format; +} spa_drm_gl_format_t; + +static const spa_drm_gl_format_t spa_drm_gl_formats[] = { + { + .spa_format = SPA_VIDEO_FORMAT_BGRA, + .drm_format = DRM_FORMAT_ARGB8888, + .gl_format = GL_BGRA_EXT, + }, + { + .spa_format = SPA_VIDEO_FORMAT_RGBA, + .drm_format = DRM_FORMAT_ABGR8888, + .gl_format = GL_RGBA, + }, + { + .spa_format = SPA_VIDEO_FORMAT_BGRx, + .drm_format = DRM_FORMAT_XRGB8888, + .gl_format = GL_BGR_EXT, + }, + { + .spa_format = SPA_VIDEO_FORMAT_RGBx, + .drm_format = DRM_FORMAT_XBGR8888, + .gl_format = GL_RGB, + }, + { + .spa_format = -1U, + .drm_format = -1U, + .gl_format = -1U + } +}; + +static const spa_drm_gl_format_t * spa_drm_gl_format_from_spa(uint32_t spa_format) { + const spa_drm_gl_format_t * format = spa_drm_gl_formats; + while (format->spa_format != -1U) { + if (format->spa_format == spa_format) { + return format; + } + + format++; + } + + return NULL; +} + +static int token_counter; +static char * generate_token() { + int pid = getpid(); + int id = ++token_counter; + + char * token_buffer; + int status = asprintf(&token_buffer, "wl_mirror_%d_%d", pid, id); + if (status == -1) { + return NULL; + } + + return token_buffer; +} + +static char * get_unique_name_identifier(xdg_portal_mirror_backend_t * backend) { + const char * unique_name; + sd_bus_get_unique_name(backend->bus, &unique_name); + if (unique_name[0] != ':') { + return NULL; + } + + size_t length = strlen(unique_name); + char * ident_buffer = malloc(length); + if (ident_buffer == NULL) { + return NULL; + } + + strncpy(ident_buffer, unique_name + 1, length); + for (size_t i = 0; i < length - 1; i++) { + if (ident_buffer[i] == '.') { + ident_buffer[i] = '_'; + } + } + + return ident_buffer; +} + +static char * generate_handle(xdg_portal_mirror_backend_t * backend, const char * prefix, const char * token) { + char * ident = get_unique_name_identifier(backend); + if (ident == NULL) { + return NULL; + } + + char * handle_buffer; + int status = asprintf(&handle_buffer, "%s/%s/%s", prefix, ident, token); + if (status == -1) { + free(ident); + return NULL; + } + + free(ident); + return handle_buffer; +} + +static char * get_window_handle(ctx_t * ctx) { + const char * prefix = ctx->wl.xdg_exported_handle != NULL ? "wayland:" : ""; + const char * name = ctx->wl.xdg_exported_handle != NULL ? ctx->wl.xdg_exported_handle : ""; + + char * handle_buffer; + int status = asprintf(&handle_buffer, "%s%s", prefix, name); + if (status == -1) { + return NULL; + } + + return handle_buffer; +} + +// --- dbus method call helpers --- + +static int on_request_response(sd_bus_message * reply, void * data, sd_bus_error * err); +static char * register_new_request(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + (void)ctx; + + char * token = generate_token(); + if (token == NULL) { + wlm_log_error("mirror-xdg-portal::reqister_new_request(): failed to allocate request token\n"); + return NULL; + } + + char * request_handle = generate_handle(backend, "/org/freedesktop/portal/desktop/request", token); + if (request_handle == NULL) { + wlm_log_error("mirror-xdg-portal::reqister_new_request(): failed to allocate request handle\n"); + free(token); + return NULL; + } + + if (sd_bus_match_signal_async(backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + request_handle, + "org.freedesktop.portal.Request", "Response", + on_request_response, NULL, (void *)&backend->rctx + ) < 0) { + wlm_log_error("mirror-xdg-portal::reqister_new_request(): failed to register response listener\n"); + free(token); + free(request_handle); + return NULL; + } + + backend->request_handle = request_handle; + return token; +} + +static int on_session_closed(sd_bus_message * reply, void * data, sd_bus_error * err); +static char * register_new_session(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + char * token = generate_token(); + if (token == NULL) { + wlm_log_error("mirror-xdg-portal::reqister_new_session(): failed to allocate session token\n"); + return NULL; + } + + char * session_handle = generate_handle(backend, "/org/freedesktop/portal/desktop/session", token); + if (session_handle == NULL) { + wlm_log_error("mirror-xdg-portal::reqister_new_session(): failed to allocate session handle\n"); + free(token); + return NULL; + } + + if (sd_bus_match_signal_async(backend->bus, &backend->session_slot, + "org.freedesktop.portal.Desktop", + backend->session_handle, + "org.freedesktop.portal.Session", "Closed", + on_session_closed, NULL, (void *)ctx + ) < 0) { + wlm_log_error("mirror-xdg-portal::reqister_new_session(): failed to register session closed listener\n"); + free(token); + free(session_handle); + return NULL; + } + + backend->session_handle = session_handle; + return token; +} + +static int on_request_reply(sd_bus_message * reply, void * data, sd_bus_error * err) { + request_ctx_t * rctx = (request_ctx_t *)data; + ctx_t * ctx = rctx->ctx; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + sd_bus_slot_unref(backend->call_slot); + backend->call_slot = NULL; + + if (sd_bus_message_is_method_error(reply, NULL)) { + sd_bus_error_copy(err, sd_bus_message_get_error(reply)); + wlm_log_error("mirror-xdg-portal::on_request_reply(): dbus error received: %s, %s\n", err->name, err->name); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + const char * request_handle = NULL; + if (sd_bus_message_read_basic(reply, 'o', &request_handle) < 0) { + wlm_log_error("mirror-xdg-portal::on_request_reply(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + if (backend->request_handle == NULL) { + wlm_log_error("mirror-xdg-portal::on_request_reply(): no ongoing request\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + if (strcmp(backend->request_handle, request_handle) != 0) { + wlm_log_error("mirror-xdg-portal::on_request_reply(): request handles differ: expected %s, got %s\n", + backend->request_handle, request_handle + ); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + return 0; +} + +static int on_request_response(sd_bus_message * reply, void * data, sd_bus_error * err) { + request_ctx_t * rctx = (request_ctx_t *)data; + ctx_t * ctx = rctx->ctx; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + if (sd_bus_message_is_method_error(reply, NULL)) { + sd_bus_error_copy(err, sd_bus_message_get_error(reply)); + wlm_log_error("mirror-xdg-portal::on_request_response(): dbus error received: %s, %s\n", err->name, err->name); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + uint32_t status; + if (sd_bus_message_read_basic(reply, 'u', &status) < 0) { + wlm_log_error("mirror-xdg-portal::on_request_response(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } else if (status != 0) { + wlm_log_error("mirror-xdg-portal::on_request_response(): request %s failed: %s\n", + rctx->name, + (status == 1) ? "canceled" : + (status == 2) ? "other error" : + "unknown error" + ); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + if (backend->request_handle == NULL) { + wlm_log_error("mirror-xdg-portal::on_request_response(): no ongoing request\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + free(backend->request_handle); + backend->request_handle = NULL; + + request_ctx_handler_t handler = rctx->handler; + *rctx = (request_ctx_t) { + .ctx = NULL, + .name = NULL, + .handler = NULL + }; + + return handler(ctx, backend, reply); +} + +static int on_get_properties_reply(sd_bus_message * reply, void * data, sd_bus_error * err); +static void screencast_get_properties(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_GET_PROPERTIES; + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_get_properties(): getting properties\n"); + + if (sd_bus_call_method_async( + backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.DBus.Properties", "GetAll", + on_get_properties_reply, (void *)ctx, + "s", + "org.freedesktop.portal.ScreenCast" + ) < 0) { + wlm_log_error("mirror-xdg-portal::screencast_get_properties(): failed to call method\n"); + wlm_mirror_backend_fail_async(backend); + } +} + +static void screencast_create_session(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static int on_get_properties_reply(sd_bus_message * reply, void * data, sd_bus_error * err) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + sd_bus_slot_unref(backend->call_slot); + backend->call_slot = NULL; + + if (sd_bus_message_is_method_error(reply, NULL)) { + sd_bus_error_copy(err, sd_bus_message_get_error(reply)); + wlm_log_error("mirror-xdg-portal::on_get_properties_reply(): dbus error received: %s, %s\n", err->name, err->name); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + char * keys[3]; + uint32_t values[3]; + if (sd_bus_message_read(reply, "a{sv}", 3, + &keys[0], "u", &values[0], + &keys[1], "u", &values[1], + &keys[2], "u", &values[2] + ) < 0) { + wlm_log_error("mirror-xdg-portal::on_get_properties_reply(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + screencast_properties_t properties = (screencast_properties_t){ + .source_types = 0, + .cursor_modes = 0, + .version = 0 + }; + for (int i = 0; i < 3; i++) { + if (strcmp(keys[i], "AvailableSourceTypes") == 0) properties.source_types = values[i]; + else if (strcmp(keys[i], "AvailableCursorModes") == 0) properties.cursor_modes = values[i]; + else if (strcmp(keys[i], "version") == 0) properties.version = values[i]; + else { + wlm_log_warn("mirror-xdg-portal::on_get_properties_reply(): unknown property: %s\n", keys[i]); + } + } + + if (properties.version < SCREENCAST_MIN_VERSION) { + wlm_log_error("mirror-xdg-portal::on_get_properties_reply(): got interface version %d, need at least %d\n", + properties.version, SCREENCAST_MIN_VERSION + ); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::on_get_properties_reply(): source types: {%s%s%s }\n", + (properties.source_types & SCREENCAST_MONITOR) ? " MONITOR," : "", + (properties.source_types & SCREENCAST_WINDOW) ? " WINDOW," : "", + (properties.source_types & SCREENCAST_VIRTUAL) ? " VIRTUAL," : "" + ); + wlm_log_debug(ctx, "mirror-xdg-portal::on_get_properties_reply(): cursor modes: {%s%s%s }\n", + (properties.cursor_modes & SCREENCAST_HIDDEN) ? " HIDDEN," : "", + (properties.cursor_modes & SCREENCAST_EMBEDDED) ? " EMBEDDED," : "", + (properties.cursor_modes & SCREENCAST_METADATA) ? " METADATA," : "" + ); + wlm_log_debug(ctx, "mirror-xdg-portal::on_get_properties_reply(): version: %d\n", properties.version); + backend->screencast_properties = properties; + + screencast_create_session(ctx, backend); + + return 0; +} + +static int on_session_closed(sd_bus_message * reply, void * data, sd_bus_error * err); +static int on_create_session_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply); +static void screencast_create_session(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_CREATE_SESSION; + + char * request_token = register_new_request(ctx, backend); + if (request_token == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_create_session(): failed to register request\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + char * session_token = register_new_session(ctx, backend); + if (session_token == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_create_session(): failed to register session\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + return; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_create_session(): creating session with request=%s, session=%s\n", + request_token, session_token + ); + + backend->rctx = (request_ctx_t) { + .ctx = ctx, + .name = "CreateSession", + .handler = on_create_session_response + }; + if (sd_bus_call_method_async(backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", "CreateSession", + on_request_reply, (void *)&backend->rctx, + "a{sv}", 2, + "handle_token", "s", request_token, + "session_handle_token", "s", session_token + ) < 0) { + wlm_log_error("mirror-xdg-portal::screencast_create_session(): failed to call method\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + free(session_token); + return; + } + + free(request_token); + free(session_token); +} + +static int on_session_closed(sd_bus_message * reply, void * data, sd_bus_error * err) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + if (sd_bus_message_is_method_error(reply, NULL)) { + sd_bus_error_copy(err, sd_bus_message_get_error(reply)); + wlm_log_error("mirror-xdg-portal::on_session_closed(): dbus error received: %s, %s\n", err->name, err->name); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + backend->session_open = false; + + wlm_log_error("mirror-xdg-portal::on_session_closed(): session closed unexpectedly\n"); + wlm_mirror_backend_fail_async(backend); + return 1; +} + +static void screencast_select_sources(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static int on_create_session_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply) { + const char * key; + const char * session_handle; + if (sd_bus_message_read(reply, "a{sv}", 1, + &key, "s", &session_handle + ) < 0) { + wlm_log_error("mirror-xdg-portal::on_create_session_response(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } else if (strcmp(key, "session_handle") != 0) { + wlm_log_warn("mirror-xdg-portal::on_create_session_response(): unexpected key: %s\n", key); + } + + if (backend->session_handle == NULL) { + wlm_log_error("mirror-xdg-portal::on_create_session_response(): no ongoing session\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + if (strcmp(backend->session_handle, session_handle) != 0) { + wlm_log_error("mirror-xdg-portal::on_create_session_response(): session handles differ: expected %s, got %s\n", + backend->session_handle, session_handle + ); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + backend->session_open = true; + + screencast_select_sources(ctx, backend); + return 0; +} + +static int on_select_sources_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply); +static void screencast_select_sources(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_SELECT_SOURCES; + + char * request_token = register_new_request(ctx, backend); + if (request_token == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_select_sources(): failed to register request\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_select_sources(): selecting sources with request=%s\n", + request_token + ); + + uint32_t source_type = 0; + if (backend->screencast_properties.source_types & SCREENCAST_MONITOR) source_type = SCREENCAST_MONITOR; + else if (backend->screencast_properties.source_types & SCREENCAST_WINDOW) source_type = SCREENCAST_WINDOW; + else if (backend->screencast_properties.source_types & SCREENCAST_VIRTUAL) source_type = SCREENCAST_VIRTUAL; + else { + wlm_log_error("mirror-xdg-portal::screencast_select_sources(): failed to find valid source type\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + return; + } + + uint32_t requested_cursor_mode = (ctx->opt.show_cursor ? SCREENCAST_EMBEDDED : SCREENCAST_HIDDEN); + uint32_t cursor_mode = 0; + if (backend->screencast_properties.cursor_modes & requested_cursor_mode) cursor_mode = requested_cursor_mode; + else if (backend->screencast_properties.source_types & SCREENCAST_EMBEDDED) cursor_mode = SCREENCAST_EMBEDDED; + else if (backend->screencast_properties.source_types & SCREENCAST_HIDDEN) cursor_mode = SCREENCAST_HIDDEN; + else { + wlm_log_error("mirror-xdg-portal::screencast_select_sources(): failed to find valid cursor mode\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + return; + } + + backend->rctx = (request_ctx_t) { + .ctx = ctx, + .name = "SelectSources", + .handler = on_select_sources_response + }; + if (sd_bus_call_method_async(backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", "SelectSources", + on_request_reply, (void *)&backend->rctx, + "oa{sv}", + backend->session_handle, + 3, + "handle_token", "s", request_token, + "types", "u", source_type, + "cursor_mode", "u", cursor_mode + ) < 0) { + wlm_log_error("mirror-xdg-portal::screencast_select_sources(): failed to call method\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + return; + } + + free(request_token); +} + +static void screencast_start(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static int on_select_sources_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_select_sources_response(): sources selected successfully\n"); + (void)reply; + + screencast_start(ctx, backend); + return 0; +} + +static int on_start_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply); +static void screencast_start(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_START; + + char * request_token = register_new_request(ctx, backend); + if (request_token == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_start(): failed to register request\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + char * window_handle = get_window_handle(ctx); + if (window_handle == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_start(): failed to allocate window handle\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + return; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_start(): starting with request=%s, window=%s\n", + request_token, window_handle + ); + + backend->rctx = (request_ctx_t) { + .ctx = ctx, + .name = "Start", + .handler = on_start_response + }; + if (sd_bus_call_method_async(backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", "Start", + on_request_reply, (void *)&backend->rctx, + "osa{sv}", + backend->session_handle, + window_handle, + 1, + "handle_token", "s", request_token + ) < 0) { + wlm_log_error("mirror-xdg-portal::screencast_start(): failed to call method\n"); + wlm_mirror_backend_fail_async(backend); + + free(request_token); + free(window_handle); + return; + } + + free(request_token); + free(window_handle); +} + +static void screencast_open_pipewire(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static int on_start_response(ctx_t * ctx, xdg_portal_mirror_backend_t * backend, sd_bus_message * reply) { + int ret; + + uint32_t pw_node_id; + int32_t x = -1, y = -1, w = -1, h = -1; + + if (sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "{sv}") < 0) goto fail; + for (int i = 0; (ret = sd_bus_message_enter_container(reply, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0; i++) { + const char * result_key; + if (sd_bus_message_read_basic(reply, 's', &result_key) < 0) goto fail; + + if (strcmp(result_key, "streams") == 0) { + if (sd_bus_message_enter_container(reply, SD_BUS_TYPE_VARIANT, "a(ua{sv})") < 0) goto fail; + if (sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ua{sv})") < 0) goto fail; + for (int j = 0; (ret = sd_bus_message_enter_container(reply, SD_BUS_TYPE_STRUCT, "ua{sv}")) > 0 && j < 1; j++) { + if (sd_bus_message_read_basic(reply, 'u', &pw_node_id) < 0) goto fail; + if (sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "{sv}") < 0) goto fail; + for (int k = 0; (ret = sd_bus_message_enter_container(reply, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0; k++) { + const char * prop_key; + if (sd_bus_message_read_basic(reply, 's', &prop_key) < 0) goto fail; + + if (strcmp(prop_key, "position") == 0) { + if (sd_bus_message_read(reply, "v", "(ii)", &x, &y) < 0) goto fail; + } else if (strcmp(prop_key, "size") == 0) { + if (sd_bus_message_read(reply, "v", "(ii)", &w, &h) < 0) goto fail; + } else { + if (sd_bus_message_skip(reply, "v") < 0) goto fail; + } + + if (sd_bus_message_exit_container(reply) < 0) goto fail; + } + if (ret < 0) goto fail; + if (sd_bus_message_exit_container(reply) < 0) goto fail; + if (sd_bus_message_exit_container(reply) < 0) goto fail; + } + if (ret < 0) goto fail; + if (ret > 0) goto too_many; + if (sd_bus_message_exit_container(reply) < 0) goto fail; + if (sd_bus_message_exit_container(reply) < 0) goto fail; + } else { + if (sd_bus_message_skip(reply, "v") < 0) goto fail; + } + + if (sd_bus_message_exit_container(reply) < 0) goto fail; + } + if (ret < 0) goto fail; + if (sd_bus_message_exit_container(reply) < 0) goto fail; + + wlm_log_debug(ctx, "mirror-xdg-portal::on_start_response(): got pw_node_id=%d, x=%d, y=%d, w=%d, h=%d\n", + pw_node_id, x, y, w, h + ); + + backend->pw_node_id = pw_node_id; + backend->x = x; + backend->y = y; + backend->w = w; + backend->h = h; + + screencast_open_pipewire(ctx, backend); + return 0; + +too_many: + wlm_log_error("mirror-xdg-portal::on_start_response(): to many array entries, expected 1\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + +fail: + wlm_log_error("mirror-xdg-portal::on_start_response(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; +} + +static int on_open_pipewire_reply(sd_bus_message * reply, void * data, sd_bus_error * err); +static void screencast_open_pipewire(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_OPEN_PIPEWIRE_REMOTE; + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_open_pipewire(): opening pipewire remote\n"); + + if (sd_bus_call_method_async(backend->bus, &backend->call_slot, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", "OpenPipeWireRemote", + on_open_pipewire_reply, (void *)ctx, + "oa{sv}", + backend->session_handle, + 0 + ) < 0) { + wlm_log_error("mirror-xdg-portal::screencast_open_pipewire(): failed to call method\n"); + wlm_mirror_backend_fail_async(backend); + } +} + +static void screencast_pipewire_init(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static int on_open_pipewire_reply(sd_bus_message * reply, void * data, sd_bus_error * err) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + sd_bus_slot_unref(backend->call_slot); + backend->call_slot = NULL; + + if (sd_bus_message_is_method_error(reply, NULL)) { + sd_bus_error_copy(err, sd_bus_message_get_error(reply)); + wlm_log_error("mirror-xdg-portal::on_open_pipewire_reply(): dbus error received: %s, %s\n", err->name, err->name); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + int fd; + if (sd_bus_message_read_basic(reply, 'h', &fd) < 0) { + wlm_log_error("mirror-xdg-portal::on_open_pipewire_reply(): failed to read reply\n"); + wlm_mirror_backend_fail_async(backend); + return 1; + } + + backend->pw_fd = dup(fd); + if (backend->pw_fd == -1) { + wlm_log_error("mirror-xdg-portal::on_open_pipewire_reply(): failed to duplicate pipewire fd\n"); + } + + wlm_log_debug(ctx, "mirror-xdg-portal::on_open_pipewire_reply(): received pipewire fd: %d\n", backend->pw_fd); + + screencast_pipewire_init(ctx, backend); + return 0; +} + +// --- pipewire functions --- + +static void on_pw_core_info(void * data, const struct pw_core_info * info); +static void on_pw_core_error(void * data, uint32_t id, int seq, int res, const char * msg); + +static const struct pw_core_events pw_core_events = { + .version = PW_VERSION_CORE_EVENTS, + .info = on_pw_core_info, + .error = on_pw_core_error +}; + +static void screencast_pipewire_init(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_PW_INIT; + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_pipewire_init(): initializing pipewire event loop\n"); + + backend->pw_core = pw_context_connect_fd(backend->pw_context, backend->pw_fd, NULL, 0); + backend->pw_fd = -1; + if (backend->pw_core == NULL) { + wlm_log_error("mirror-xdg-portal::screencast_pipewire_init(): failed to create pipewire core\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + // add core event listener for info callback to check version + pw_core_add_listener(backend->pw_core, &backend->pw_core_listener, &pw_core_events, (void *)ctx); + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_pipewire_init(): pipewire core created\n"); + +} + +static void screencast_pipewire_create_stream(ctx_t * ctx, xdg_portal_mirror_backend_t * backend); +static void on_pw_core_info(void * data, const struct pw_core_info * info) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_core_info(): pipewire version = %s\n", info->version); + + uint32_t major, minor, patch; + if (sscanf(info->version, "%d.%d.%d", &major, &minor, &patch) != 3) { + wlm_log_error("mirror-xdg-portal::on_pw_core_info(): failed to parse pipewire version\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + backend->pw_major = major; + backend->pw_minor = minor; + backend->pw_patch = patch; + + screencast_pipewire_create_stream(ctx, backend); +} + +static bool screencast_pipewire_check_version(xdg_portal_mirror_backend_t * backend, uint32_t major, uint32_t minor, uint32_t patch) { + return backend->pw_major == major && backend->pw_minor == minor && backend->pw_patch >= patch; +} + +static void on_pw_core_error(void * data, uint32_t id, int seq, int res, const char * msg) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + wlm_log_error("mirror-xdg-portal::on_pw_core_error(): got error: id = %d, seq = %d, res = %d, msg = %s\n", id, seq, res, msg); + wlm_mirror_backend_fail_async(backend); +} + +static void on_pw_stream_process(void * data); +static void on_pw_param_changed(void * data, uint32_t id, const struct spa_pod * param); +static void on_pw_state_changed(void * data, enum pw_stream_state old, enum pw_stream_state new, const char * error); + +static const struct pw_stream_events pw_stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = on_pw_state_changed, + .param_changed = on_pw_param_changed, + .process = on_pw_stream_process +}; + +static void screencast_pipewire_create_stream(ctx_t * ctx, xdg_portal_mirror_backend_t * backend) { + backend->state = STATE_PW_CREATE_STREAM; + + wlm_log_debug(ctx, "mirror-xdg-portal::screencast_pipewire_create_stream(): creating video stream\n"); + + backend->pw_stream = pw_stream_new( + backend->pw_core, "wl-mirror", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Screen", + NULL + ) + ); + + pw_stream_add_listener(backend->pw_stream, &backend->pw_stream_listener, &pw_stream_events, (void *)ctx); + + struct spa_pod_dynamic_builder pod_builders[8]; + spa_pod_dynamic_builder_init(&pod_builders[0], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[1], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[2], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[3], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[4], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[5], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[6], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[7], NULL, 0, 1); + +#define ADD_FORMAT(builder, spa_format, ...) ({ \ + struct spa_pod_builder * b = builder; \ + const uint64_t * modifiers = (uint64_t[]){ __VA_ARGS__ }; \ + size_t num_modifiers = ARRAY_LENGTH(((uint64_t[]){ __VA_ARGS__ })); \ + \ + struct spa_pod_frame format_frame; \ + spa_pod_builder_push_object(b, &format_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); \ + spa_pod_builder_add(b, \ + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), \ + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), \ + SPA_FORMAT_VIDEO_format, SPA_POD_Id(spa_format), \ + 0 \ + ); \ + \ + if (num_modifiers > 0) { \ + spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); \ + struct spa_pod_frame modifier_frame; \ + spa_pod_builder_push_choice(b, &modifier_frame, SPA_CHOICE_Enum, 0); \ + spa_pod_builder_long(b, modifiers[0]); \ + for (uint32_t i = 0; i < num_modifiers; i++) { \ + spa_pod_builder_long(b, modifiers[i]); \ + } \ + spa_pod_builder_pop(b, &modifier_frame); \ + } \ + \ + spa_pod_builder_add(b, \ + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( \ + &SPA_RECTANGLE(320, 240), /* arbitrary */ \ + &SPA_RECTANGLE(1, 1), /* min */ \ + &SPA_RECTANGLE(8192, 4320) /* max */ \ + ), \ + SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( \ + &SPA_FRACTION(30 /* ovi->fps_num */, 1 /* ovi->fps_den */), \ + &SPA_FRACTION(0, 1), \ + &SPA_FRACTION(360, 1) \ + ), \ + 0 \ + ); \ + spa_pod_builder_pop(b, &format_frame); \ + }) + + // TODO: don't hardcode video format options + const struct spa_pod * params[] = { + ADD_FORMAT(&pod_builders[0].b, SPA_VIDEO_FORMAT_ABGR, 0x0000000000000000, 0x0100000000000001, 0x0100000000000002, 0x0100000000000004, 0x00ffffffffffffff), + ADD_FORMAT(&pod_builders[1].b, SPA_VIDEO_FORMAT_ARGB, 0x0000000000000000, 0x0100000000000001, 0x0100000000000002, 0x0100000000000004, 0x00ffffffffffffff), + ADD_FORMAT(&pod_builders[2].b, SPA_VIDEO_FORMAT_BGRx, 0x0000000000000000, 0x0100000000000001, 0x0100000000000002, 0x0100000000000004, 0x00ffffffffffffff), + ADD_FORMAT(&pod_builders[3].b, SPA_VIDEO_FORMAT_RGBx, 0x0000000000000000, 0x0100000000000001, 0x0100000000000002, 0x0100000000000004, 0x00ffffffffffffff), + ADD_FORMAT(&pod_builders[4].b, SPA_VIDEO_FORMAT_ABGR), + ADD_FORMAT(&pod_builders[5].b, SPA_VIDEO_FORMAT_ARGB), + ADD_FORMAT(&pod_builders[6].b, SPA_VIDEO_FORMAT_BGRx), + ADD_FORMAT(&pod_builders[7].b, SPA_VIDEO_FORMAT_RGBx) + }; + uint32_t num_params = ARRAY_LENGTH(params); + + pw_stream_connect( + backend->pw_stream, PW_DIRECTION_INPUT, backend->pw_node_id, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, + params, num_params + ); + + spa_pod_dynamic_builder_clean(&pod_builders[0]); + spa_pod_dynamic_builder_clean(&pod_builders[1]); + spa_pod_dynamic_builder_clean(&pod_builders[2]); + spa_pod_dynamic_builder_clean(&pod_builders[3]); + spa_pod_dynamic_builder_clean(&pod_builders[4]); + spa_pod_dynamic_builder_clean(&pod_builders[5]); + spa_pod_dynamic_builder_clean(&pod_builders[6]); + spa_pod_dynamic_builder_clean(&pod_builders[7]); +} + +static void on_pw_stream_process(void * data) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + if (backend->state != STATE_RUNNING) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): called while not running\n"); + return; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): new video data\n"); + + struct pw_buffer * buffer = NULL; + struct pw_buffer * next_buffer = NULL; + while ((next_buffer = pw_stream_dequeue_buffer(backend->pw_stream)) != NULL) { + if (buffer != NULL) { + pw_stream_queue_buffer(backend->pw_stream, buffer); + } + + buffer = next_buffer; + } + + if (buffer == NULL) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): out of buffers\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + struct spa_buffer * spa_buffer = buffer->buffer; + struct spa_meta_header * meta = spa_buffer_find_meta_data(spa_buffer, + SPA_META_Header, sizeof (struct spa_meta_header) + ); + if (meta != NULL) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): received meta header\n"); + + if ((meta->flags & SPA_META_HEADER_FLAG_CORRUPTED) != 0) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): received corrupt buffer\n"); + pw_stream_queue_buffer(backend->pw_stream, buffer); + wlm_mirror_backend_fail_async(backend); + return; + } + } + + if (spa_buffer->datas[0].type == SPA_DATA_DmaBuf) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): received DMA buf\n"); + + if (spa_buffer->n_datas > MAX_PLANES) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): max %d planes are supported, got %d planes\n", MAX_PLANES, spa_buffer->n_datas); + pw_stream_queue_buffer(backend->pw_stream, buffer); + wlm_mirror_backend_fail_async(backend); + return; + } + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): w=%d h=%d gl_format=%x drm_format=%c%c%c%c drm_modifier=%016lx\n", + backend->w, backend->h, backend->gl_format, + (backend->drm_format >> 0) & 0xFF, + (backend->drm_format >> 8) & 0xFF, + (backend->drm_format >> 16) & 0xFF, + (backend->drm_format >> 24) & 0xFF, + backend->drm_modifier + ); + + dmabuf_t dmabuf; + dmabuf.width = backend->w; + dmabuf.height = backend->h; + dmabuf.drm_format = backend->drm_format; + dmabuf.planes = spa_buffer->n_datas; + dmabuf.fds = malloc(dmabuf.planes * sizeof (int)); + dmabuf.offsets = malloc(dmabuf.planes * sizeof (uint32_t)); + dmabuf.strides = malloc(dmabuf.planes * sizeof (uint32_t)); + dmabuf.modifier = backend->drm_modifier; + if (dmabuf.fds == NULL || dmabuf.offsets == NULL || dmabuf.strides == NULL) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): failed to allocate dmabuf storage\n"); + pw_stream_queue_buffer(backend->pw_stream, buffer); + wlm_mirror_backend_fail_async(backend); + return; + } + + bool corrupted = false; + for (size_t i = 0; i < dmabuf.planes; i++) { + dmabuf.fds[i] = spa_buffer->datas[i].fd; + dmabuf.offsets[i] = spa_buffer->datas[i].chunk->offset; + dmabuf.strides[i] = spa_buffer->datas[i].chunk->stride; + corrupted |= (bool)(spa_buffer->datas[i].chunk->flags & SPA_CHUNK_FLAG_CORRUPTED); + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): fd=%d offset=% 10d stride=% 10d type=%d\n", + dmabuf.fds[i], dmabuf.offsets[i], dmabuf.strides[i], spa_buffer->datas[i].type + ); + } + + if (corrupted) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): received corrupt dmabuf\n"); + pw_stream_queue_buffer(backend->pw_stream, buffer); + wlm_mirror_backend_fail_async(backend); + free(dmabuf.fds); + free(dmabuf.offsets); + free(dmabuf.strides); + return; + } + + if (!wlm_egl_dmabuf_to_texture(ctx, &dmabuf)) { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): failed to import dmabuf\n"); + pw_stream_queue_buffer(backend->pw_stream, buffer); + wlm_mirror_backend_fail_async(backend); + free(dmabuf.fds); + free(dmabuf.offsets); + free(dmabuf.strides); + return; + } + + free(dmabuf.fds); + free(dmabuf.offsets); + free(dmabuf.strides); + ctx->egl.format = backend->gl_format; + ctx->egl.texture_region_aware = false; + + if (ctx->mirror.invert_y != false) { + ctx->mirror.invert_y = false; + wlm_egl_update_uniforms(ctx); + } + + backend->header.fail_count = 0; + } else if (spa_buffer->datas[0].type == SPA_DATA_MemPtr) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): received SHM buf\n"); + + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): SHM buffers not yet implemented\n"); + wlm_mirror_backend_fail_async(backend); + } else { + wlm_log_error("mirror-xdg-portal::on_pw_stream_process(): received unknown buffer type\n"); + wlm_mirror_backend_fail_async(backend); + } + + struct spa_meta_region * region = spa_buffer_find_meta_data(spa_buffer, + SPA_META_VideoCrop, sizeof (struct spa_meta_region) + ); + if (region != NULL && spa_meta_region_is_valid(region)) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): received meta region\n"); + // TODO: handle region + } + + struct spa_meta_videotransform * transform = spa_buffer_find_meta_data(spa_buffer, + SPA_META_VideoTransform, sizeof (struct spa_meta_videotransform) + ); + if (transform != NULL) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_stream_process(): received meta transform\n"); + // TODO: handle transform + } + + pw_stream_queue_buffer(backend->pw_stream, buffer); +} + +static void on_pw_param_changed(void * data, uint32_t id, const struct spa_pod * param) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + if (backend->state != STATE_RUNNING && backend->state != STATE_PW_CREATE_STREAM) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_param_changed(): called while not running\n"); + return; + } + + if (id == SPA_PARAM_Format) { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_param_changed(): format changed\n"); + + uint32_t media_type; + uint32_t media_subtype; + if (spa_format_parse(param, &media_type, &media_subtype) < 0) { + wlm_log_error("mirror-xdg-portal::on_pw_param_changed(): failed to parse SPA format\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + + if (media_type != SPA_MEDIA_TYPE_video && media_subtype != SPA_MEDIA_SUBTYPE_raw) { + wlm_log_error("mirror-xdg-portal::on_pw_param_changed(): unsupported media type '%d.%d'\n", media_type, media_subtype); + wlm_mirror_backend_fail_async(backend); + return; + } + + struct spa_video_info_raw info_raw; + if (spa_format_video_raw_parse(param, &info_raw) < 0) { + wlm_log_error("mirror-xdg-portal::on_pw_param_changed(): failed to parse SPA raw format\n"); + wlm_mirror_backend_fail_async(backend); + return; + } + backend->w = info_raw.size.width; + backend->h = info_raw.size.height; + backend->drm_modifier = info_raw.modifier; + + const spa_drm_gl_format_t * format = spa_drm_gl_format_from_spa(info_raw.format); + if (format == NULL) { + wlm_log_error("mirror-xdg-portal::on_pw_param_changed(): unsupported SPA format '%d'\n", info_raw.format); + wlm_mirror_backend_fail_async(backend); + return; + } + backend->gl_format = format->gl_format; + backend->drm_format = format->drm_format; + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_param_changed(): w=%d h=%d gl_format=%x drm_format=%c%c%c%c drm_modifier=%016lx\n", + backend->w, backend->h, backend->gl_format, + (backend->drm_format >> 0) & 0xFF, + (backend->drm_format >> 8) & 0xFF, + (backend->drm_format >> 16) & 0xFF, + (backend->drm_format >> 24) & 0xFF, + backend->drm_modifier + ); + + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_param_changed(): spa_format=%d framerate=%d/%d\n", + info_raw.format, info_raw.framerate.num, info_raw.framerate.denom + ); + + uint32_t supported_buffer_types = (1 << SPA_DATA_MemPtr); + bool has_modifier = spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier) != NULL; + if (has_modifier || screencast_pipewire_check_version(backend, 0, 3, 24)) { + supported_buffer_types |= (1 << SPA_DATA_DmaBuf); + } + + struct spa_pod_dynamic_builder pod_builders[4]; + spa_pod_dynamic_builder_init(&pod_builders[0], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[1], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[2], NULL, 0, 1); + spa_pod_dynamic_builder_init(&pod_builders[3], NULL, 0, 1); + + const struct spa_pod * params[] = { + // buffer options + spa_pod_builder_add_object(&pod_builders[0].b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(supported_buffer_types) + ), + // meta header + spa_pod_builder_add_object(&pod_builders[1].b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_header)) + ), + // region + spa_pod_builder_add_object(&pod_builders[2].b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), + SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region)) + ), + // transform + spa_pod_builder_add_object(&pod_builders[3].b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), + SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_videotransform)) + ), + }; + uint32_t num_params = ARRAY_LENGTH(params); + + pw_stream_update_params(backend->pw_stream, params, num_params); + + spa_pod_dynamic_builder_clean(&pod_builders[0]); + spa_pod_dynamic_builder_clean(&pod_builders[1]); + spa_pod_dynamic_builder_clean(&pod_builders[2]); + spa_pod_dynamic_builder_clean(&pod_builders[3]); + + // TODO: remember that negotiation already happened + backend->state = STATE_RUNNING; + } else { + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_param_changed(): unknown param id = %d\n", id); + } + + (void)backend; +} + +static void on_pw_state_changed(void * data, enum pw_stream_state old, enum pw_stream_state new, const char * error) { + ctx_t * ctx = (ctx_t *)data; + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + const char * old_s = pw_stream_state_as_string(old); + const char * new_s = pw_stream_state_as_string(new); + wlm_log_debug(ctx, "mirror-xdg-portal::on_pw_state_changed(): state = %s -> %s, error = %s\n", old_s, new_s, error); + + (void)backend; +} + +// --- backend event handlers --- + +static void do_capture(ctx_t * ctx) { + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + if (backend->state == STATE_IDLE) { + screencast_get_properties(ctx, backend); + } else if (backend->state == STATE_RUNNING) { + + } else if (backend->state == STATE_BROKEN) { + wlm_mirror_backend_fail(ctx); + } +} + +static void do_cleanup(ctx_t * ctx) { + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + backend->state = STATE_BROKEN; + + wlm_log_debug(ctx, "mirror-xdg-portal::do_cleanup(): destroying mirror-xdg-portal objects\n"); + + // deregister event handlers + if (backend->bus != NULL) wlm_event_remove_fd(ctx, &backend->dbus_event_handler); + if (backend->pw_loop != NULL) wlm_event_remove_fd(ctx, &backend->pw_event_handler); + + // release pipewire resources + if (backend->pw_stream != NULL) pw_stream_destroy(backend->pw_stream); + if (backend->pw_core != NULL) pw_core_disconnect(backend->pw_core); + if (backend->pw_context != NULL) pw_context_destroy(backend->pw_context); + if (backend->pw_loop != NULL) pw_loop_destroy(backend->pw_loop); + if (backend->pw_fd != -1) close(backend->pw_fd); + + // close portal session + if (backend->session_open) { + sd_bus_call_method_async( + backend->bus, NULL, + "org.freedesktop.portal.Desktop", + backend->session_handle, + "org.freedesktop.portal.Session", "Close", + NULL, NULL, + "" + ); + } + + // release sd-bus resources + if (backend->session_slot != NULL) sd_bus_slot_unref(backend->session_slot); + if (backend->call_slot != NULL) sd_bus_slot_unref(backend->call_slot); + if (backend->request_handle != NULL) free(backend->request_handle); + if (backend->session_handle != NULL) free(backend->session_handle); + if (backend->bus != NULL) sd_bus_unref(backend->bus); + + // deinitialize pipewire + pw_deinit(); + + free(backend); + ctx->mirror.backend = NULL; +} + +// --- loop event handlers --- + +static bool update_bus_events(xdg_portal_mirror_backend_t * backend) { + if ((backend->dbus_event_handler.fd = sd_bus_get_fd(backend->bus)) < 0) { + return false; + } + + int events; + if ((events = sd_bus_get_events(backend->bus)) < 0) { + return false; + } + + int epoll_events = 0; + if (events & POLLIN) epoll_events |= EPOLLIN; + if (events & POLLRDNORM) epoll_events |= EPOLLRDNORM; + if (events & POLLRDBAND) epoll_events |= EPOLLRDBAND; + if (events & POLLPRI) epoll_events |= EPOLLPRI; + if (events & POLLOUT) epoll_events |= EPOLLOUT; + if (events & POLLWRNORM) epoll_events |= EPOLLWRNORM; + if (events & POLLWRBAND) epoll_events |= EPOLLWRBAND; + if (events & POLLERR) epoll_events |= EPOLLERR; + if (events & POLLHUP) epoll_events |= EPOLLHUP; + backend->dbus_event_handler.events = epoll_events; + + uint64_t timeout; + if (sd_bus_get_timeout(backend->bus, &timeout) < 0) { + return false; + } + + if (timeout == UINT64_MAX) { + backend->dbus_event_handler.timeout_ms = -1; + } else { + backend->dbus_event_handler.timeout_ms = timeout / 1000; + } + + return true; +} + +static void on_loop_dbus_event(ctx_t * ctx) { + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + int ret; + + // process dbus events + while ((ret = sd_bus_process(backend->bus, NULL)) > 0); + + if (ret < 0) { + wlm_log_error("mirror-xdg-portal::on_loop_pw_event(): failed to process dbus events\n"); + wlm_mirror_backend_fail(ctx); + } +} + +static void on_loop_dbus_each(ctx_t * ctx) { + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + event_handler_t old_handler = backend->dbus_event_handler; + if (!update_bus_events(backend)) { + wlm_log_error("mirror-xdg-portal::on_loop_dbus_each(): failed to update dbus pollfd\n"); + wlm_mirror_backend_fail(ctx); + } + + if (old_handler.fd != backend->dbus_event_handler.fd) { + wlm_event_remove_fd(ctx, &old_handler); + wlm_event_add_fd(ctx, &backend->dbus_event_handler); + } else if (old_handler.events != backend->dbus_event_handler.events) { + wlm_event_change_fd(ctx, &backend->dbus_event_handler); + } +} + +static void on_loop_pw_event(ctx_t * ctx) { + xdg_portal_mirror_backend_t * backend = (xdg_portal_mirror_backend_t *)ctx->mirror.backend; + + pw_loop_enter(backend->pw_loop); + int ret = pw_loop_iterate(backend->pw_loop, 0); + pw_loop_leave(backend->pw_loop); + + if (ret < 0) { + wlm_log_error("mirror-xdg-portal::on_loop_pw_event(): failed to process pipewire events\n"); + wlm_mirror_backend_fail(ctx); + } +} + +// --- init_mirror_xdg_portal --- + +void wlm_mirror_xdg_portal_init(ctx_t * ctx) { + // allocate backend context structure + xdg_portal_mirror_backend_t * backend = malloc(sizeof (xdg_portal_mirror_backend_t)); + if (backend == NULL) { + wlm_log_error("mirror-xdg-portal::init(): failed to allocate backend state\n"); + return; + } + + // initialize context structure + backend->header.do_capture = do_capture; + backend->header.do_cleanup = do_cleanup; + backend->header.fail_count = 0; + + // general info + backend->x = 0; + backend->y = 0; + backend->w = 0; + backend->h = 0; + backend->gl_format = 0; + backend->drm_format = 0; + backend->drm_modifier = 0; + + // sd-bus state + backend->screencast_properties = (screencast_properties_t) { + .source_types = 0, + .cursor_modes = 0, + .version = 0 + }; + backend->request_handle = NULL; + backend->session_handle = NULL; + backend->session_open = false; + + backend->rctx = (request_ctx_t) { + .ctx = NULL, + .name = NULL, + .handler = NULL + }; + + backend->call_slot = NULL; + backend->session_slot = NULL; + backend->bus = NULL; + backend->dbus_event_handler.fd = -1; + backend->dbus_event_handler.timeout_ms = -1; + backend->dbus_event_handler.events = 0; + backend->dbus_event_handler.on_event = on_loop_dbus_event; + backend->dbus_event_handler.on_each = on_loop_dbus_each; + backend->dbus_event_handler.next = NULL; + + // pipewire state + backend->pw_fd = -1; + backend->pw_node_id = 0; + backend->pw_major = 0; + backend->pw_minor = 0; + backend->pw_patch = 0; + + backend->pw_loop = NULL; + backend->pw_context = NULL; + backend->pw_core = NULL; + backend->pw_stream = NULL; + backend->pw_event_handler.fd = -1; + backend->pw_event_handler.timeout_ms = -1; + backend->pw_event_handler.events = 0; + backend->pw_event_handler.on_event = on_loop_pw_event; + backend->pw_event_handler.on_each = NULL; + backend->pw_event_handler.next = NULL; + + backend->state = STATE_IDLE; + + // initialize pipewire + pw_init(NULL, NULL); + + // set backend object as current backend + ctx->mirror.backend = (mirror_backend_t *)backend; + + if (sd_bus_default_user(&backend->bus) < 0) { + wlm_log_error("mirror-xdg-portal::init(): failed to get DBus session bus\n"); + wlm_mirror_backend_fail(ctx); + return; + } + + if (!update_bus_events(backend)) { + wlm_log_error("mirror-xdg-portal::init(): failed to update DBus epoll events\n"); + + // clean this up here, otherwise unknown if event listener registered + sd_bus_unref(backend->bus); + backend->bus = NULL; + + wlm_mirror_backend_fail(ctx); + return; + } + + backend->pw_loop = pw_loop_new(NULL); + if (backend->pw_loop == NULL) { + wlm_log_error("mirror-xdg-portal::init(): failed to create pipewire event loop\n"); + + wlm_mirror_backend_fail(ctx); + return; + } + + backend->pw_event_handler.fd = pw_loop_get_fd(backend->pw_loop); + backend->pw_event_handler.events = EPOLLIN; + + backend->pw_context = pw_context_new(backend->pw_loop, NULL, 0); + if (backend->pw_context == NULL) { + wlm_log_error("mirror-xdg-portal::init(): failed to create pipewire context\n"); + + wlm_mirror_backend_fail(ctx); + return; + } + + wlm_event_add_fd(ctx, &backend->dbus_event_handler); + wlm_event_add_fd(ctx, &backend->pw_event_handler); +} + +#endif diff --git a/src/mirror.c b/src/mirror.c index c2a2ad8..b2d5caa 100644 --- a/src/mirror.c +++ b/src/mirror.c @@ -102,6 +102,9 @@ typedef struct { static fallback_backend_t auto_fallback_backends[] = { { "dmabuf", wlm_mirror_dmabuf_init }, { "screencopy", wlm_mirror_screencopy_init }, +#ifdef WITH_XDG_PORTAL_BACKEND + { "xdg-portal", wlm_mirror_xdg_portal_init }, +#endif { NULL, NULL } }; @@ -153,6 +156,12 @@ void wlm_mirror_backend_init(ctx_t * ctx) { case BACKEND_SCREENCOPY: wlm_mirror_screencopy_init(ctx); break; + +#ifdef WITH_XDG_PORTAL_BACKEND + case BACKEND_XDG_PORTAL: + wlm_mirror_xdg_portal_init(ctx); + break; +#endif } if (ctx->mirror.backend == NULL) wlm_exit_fail(ctx); diff --git a/src/options.c b/src/options.c index 0df97d6..9036ff3 100644 --- a/src/options.c +++ b/src/options.c @@ -58,6 +58,11 @@ bool wlm_opt_parse_backend(backend_t * backend, const char * backend_arg) { } else if (strcmp(backend_arg, "screencopy") == 0) { *backend = BACKEND_SCREENCOPY; return true; +#ifdef WITH_XDG_PORTAL_BACKEND + } else if (strcmp(backend_arg, "xdg-portal") == 0 || strcmp(backend_arg, "pipewire") == 0) { + *backend = BACKEND_XDG_PORTAL; + return true; +#endif } else { return false; } @@ -372,6 +377,10 @@ void wlm_opt_usage(ctx_t * ctx) { printf(" - auto automatically try the backends in order and use the first that works (default)\n"); printf(" - dmabuf use the wlr-export-dmabuf-unstable-v1 protocol to capture outputs\n"); printf(" - screencopy use the wlr-screencopy-unstable-v1 protocol to capture outputs\n"); +#ifdef WITH_XDG_PORTAL_BACKEND + printf(" - xdg-portal use xdg-desktop-portal and pipewire to capture outputs or windows\n"); + printf(" - pipewire alias for 'xdg-portal'\n"); +#endif printf("\n"); printf("transforms:\n"); printf(" transforms are specified as a dash-separated list of flips followed by a rotation\n"); diff --git a/src/wayland.c b/src/wayland.c index 12a5b54..1881a02 100644 --- a/src/wayland.c +++ b/src/wayland.c @@ -263,6 +263,42 @@ static void on_registry_add( registry, id, &zxdg_output_manager_v1_interface, 2 ); ctx->wl.output_manager_id = id; + + // check for outputs that still need an xdg_output + // - sway sends outputs after protocol extensions + // - gnome sends outputs before protocol extensions + output_list_node_t * cur = ctx->wl.outputs; + while (cur != NULL) { + if (cur->xdg_output != NULL) continue; + + // create xdg_output object + cur->xdg_output = (struct zxdg_output_v1 *)zxdg_output_manager_v1_get_xdg_output( + ctx->wl.output_manager, cur->output + ); + if (cur->xdg_output == NULL) { + wlm_log_error("wayland::on_registry_add(): failed to create xdg_output\n"); + wlm_exit_fail(ctx); + } + + // add xdg_output event listener + // - for logical_position event + // - for logical_size event + // - for name event + zxdg_output_v1_add_listener(cur->xdg_output, &xdg_output_listener, (void *)cur); + + cur = cur->next; + } + } else if (strcmp(interface, zxdg_exporter_v2_interface.name) == 0) { + if (ctx->wl.xdg_exporter != NULL) { + wlm_log_error("wayland::on_registry_add(): duplicate xdg_exporter\n"); + wlm_exit_fail(ctx); + } + + // bind exporter object + ctx->wl.xdg_exporter = (struct zxdg_exporter_v2 *)wl_registry_bind( + registry, id, &zxdg_exporter_v2_interface, 1 + ); + ctx->wl.xdg_exporter_id = id; } else if (strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name) == 0) { if (ctx->wl.dmabuf_manager != NULL) { wlm_log_error("wayland::on_registry_add(): duplicate dmabuf_manager\n"); @@ -334,27 +370,24 @@ static void on_registry_add( wl_output_add_listener(node->output, &output_listener, (void *)node); // check for xdg_output_manager - // - sway always sends outputs after protocol extensions - // - for simplicity, only this event order is supported - if (ctx->wl.output_manager == NULL) { - wlm_log_error("wayland::on_registry_add(): wl_output received before xdg_output_manager\n"); - wlm_exit_fail(ctx); - } + // - sway sends outputs after protocol extensions + // - gnome sends outputs before protocol extensions + if (ctx->wl.output_manager != NULL) { + // create xdg_output object + node->xdg_output = (struct zxdg_output_v1 *)zxdg_output_manager_v1_get_xdg_output( + ctx->wl.output_manager, node->output + ); + if (node->xdg_output == NULL) { + wlm_log_error("wayland::on_registry_add(): failed to create xdg_output\n"); + wlm_exit_fail(ctx); + } - // create xdg_output object - node->xdg_output = (struct zxdg_output_v1 *)zxdg_output_manager_v1_get_xdg_output( - ctx->wl.output_manager, node->output - ); - if (node->xdg_output == NULL) { - wlm_log_error("wayland::on_registry_add(): failed to create xdg_output\n"); - wlm_exit_fail(ctx); + // add xdg_output event listener + // - for logical_position event + // - for logical_size event + // - for name event + zxdg_output_v1_add_listener(node->xdg_output, &xdg_output_listener, (void *)node); } - - // add xdg_output event listener - // - for logical_position event - // - for logical_size event - // - for name event - zxdg_output_v1_add_listener(node->xdg_output, &xdg_output_listener, (void *)node); } else if (strcmp(interface, wl_seat_interface.name) == 0) { // allocate seat node seat_list_node_t * node = malloc(sizeof (seat_list_node_t)); @@ -403,6 +436,9 @@ static void on_registry_remove( } else if (id == ctx->wl.output_manager_id) { wlm_log_error("wayland::on_registry_remove(): output_manager disappeared\n"); wlm_exit_fail(ctx); + } else if (id == ctx->wl.xdg_exporter_id) { + wlm_log_error("wayland::on_registry_remove(): exporter disappeared\n"); + wlm_exit_fail(ctx); } else if (id == ctx->wl.dmabuf_manager_id) { wlm_log_error("wayland::on_registry_remove(): dmabuf_manager disappeared\n"); wlm_exit_fail(ctx); @@ -770,6 +806,28 @@ static const struct xdg_toplevel_listener xdg_toplevel_listener = { }; #endif +// --- xdg_exported event handlers --- + +static void on_xdg_exported_handle( + void * data, struct zxdg_exported_v2 * zxdg_exported_v2, const char * handle +) { + ctx_t * ctx = (ctx_t *)data; + + wlm_log_debug(ctx, "wayland::on_xdg_exported_handle(): handle '%s' received\n", handle); + ctx->wl.xdg_exported_handle = strdup(handle); + if (ctx->wl.xdg_exported_handle == NULL) { + wlm_log_error("wayland::on_xdg_exported_handle(): failed to allocate handle\n"); + wlm_exit_fail(ctx); + } + + (void)zxdg_exported_v2; + +} + +static const struct zxdg_exported_v2_listener xdg_exported_listener = { + .handle = on_xdg_exported_handle +}; + // --- wayland event loop handlers --- static void on_wayland_event(ctx_t * ctx) { @@ -849,6 +907,10 @@ void wlm_wayland_init(ctx_t * ctx) { ctx->wl.screencopy_manager = NULL; ctx->wl.screencopy_manager_id = 0; + ctx->wl.xdg_exporter = NULL; + ctx->wl.xdg_exported_surface = NULL; + ctx->wl.xdg_exporter_id = 0; + ctx->wl.outputs = NULL; ctx->wl.seats = NULL; @@ -975,13 +1037,6 @@ void wlm_wayland_init(ctx_t * ctx) { // map libdecor frame libdecor_frame_map(ctx->wl.libdecor_frame); - - // commit surface to trigger configure sequence - wl_surface_commit(ctx->wl.surface); - - // wait for events - // - expecting libdecor frame configure event - wl_display_roundtrip(ctx->wl.display); #else // create xdg surface ctx->wl.xdg_surface = xdg_wm_base_get_xdg_surface(ctx->wl.wm_base, ctx->wl.surface); @@ -1009,15 +1064,22 @@ void wlm_wayland_init(ctx_t * ctx) { // set xdg toplevel properties xdg_toplevel_set_app_id(ctx->wl.xdg_toplevel, "at.yrlf.wl_mirror"); xdg_toplevel_set_title(ctx->wl.xdg_toplevel, "Wayland Output Mirror"); +#endif + + // export toplevel + if (ctx->wl.xdg_exporter != NULL) { + ctx->wl.xdg_exported_surface = zxdg_exporter_v2_export_toplevel(ctx->wl.xdg_exporter, ctx->wl.surface); + zxdg_exported_v2_add_listener(ctx->wl.xdg_exported_surface, &xdg_exported_listener, (void *)ctx); + } // commit surface to trigger configure sequence wl_surface_commit(ctx->wl.surface); // wait for events - // - expecting surface configure event - // - expecting xdg toplevel configure event + // - with libdecor: expecting libdecor frame configure event + // - without libdecor: expecting surface configure event + // - without libdecor: expecting xdg toplevel configure event wl_display_roundtrip(ctx->wl.display); -#endif // set fullscreen on xdg_toplevel if (ctx->opt.fullscreen && ctx->opt.fullscreen_output != NULL) { @@ -1143,6 +1205,9 @@ void wlm_wayland_cleanup(ctx_t *ctx) { if (ctx->wl.viewport != NULL) wp_viewport_destroy(ctx->wl.viewport); if (ctx->wl.surface != NULL) wl_surface_destroy(ctx->wl.surface); if (ctx->wl.output_manager != NULL) zxdg_output_manager_v1_destroy(ctx->wl.output_manager); + if (ctx->wl.xdg_exported_handle != NULL) free((void *)ctx->wl.xdg_exported_handle); + if (ctx->wl.xdg_exported_surface != NULL) zxdg_exported_v2_destroy(ctx->wl.xdg_exported_surface); + if (ctx->wl.xdg_exporter != NULL) zxdg_exporter_v2_destroy(ctx->wl.xdg_exporter); if (ctx->wl.wm_base != NULL) xdg_wm_base_destroy(ctx->wl.wm_base); if (ctx->wl.fractional_scale_manager != NULL) wp_fractional_scale_manager_v1_destroy(ctx->wl.fractional_scale_manager); if (ctx->wl.viewporter != NULL) wp_viewporter_destroy(ctx->wl.viewporter);