/* * Copyright (C) 2021-2024 Red Hat Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * */ #include "config.h" #include "remote-desktop-utils.h" #include #include #include #include #include #define CURSOR_META_SIZE(width, height) \ (sizeof(struct spa_meta_cursor) + \ sizeof(struct spa_meta_bitmap) + width * height * 4) typedef struct _PipeWireSource { GSource source; struct pw_loop *pipewire_loop; } PipeWireSource; static GSource *_pipewire_source; static struct pw_context *_pipewire_context; static struct pw_core *_pipewire_core; static struct spa_hook _pipewire_core_listener; static gboolean pipewire_loop_source_prepare (GSource *source, int *timeout) { *timeout = -1; return FALSE; } static gboolean pipewire_loop_source_dispatch (GSource *source, GSourceFunc callback, gpointer user_data) { PipeWireSource *pipewire_source = (PipeWireSource *) source; int result; result = pw_loop_iterate (pipewire_source->pipewire_loop, 0); if (result < 0) g_error ("pipewire_loop_iterate failed: %s", spa_strerror (result)); return TRUE; } static void pipewire_loop_source_finalize (GSource *source) { PipeWireSource *pipewire_source = (PipeWireSource *) source; pw_loop_leave (pipewire_source->pipewire_loop); pw_loop_destroy (pipewire_source->pipewire_loop); } static GSourceFuncs pipewire_source_funcs = { pipewire_loop_source_prepare, NULL, pipewire_loop_source_dispatch, pipewire_loop_source_finalize }; static GSource * create_pipewire_source (struct pw_loop *pipewire_loop) { GSource *source; PipeWireSource *pipewire_source; source = g_source_new (&pipewire_source_funcs, sizeof (PipeWireSource)); pipewire_source = (PipeWireSource *) source; pipewire_source->pipewire_loop = pipewire_loop; g_source_add_unix_fd (source, pw_loop_get_fd (pipewire_source->pipewire_loop), G_IO_IN | G_IO_ERR); pw_loop_enter (pipewire_source->pipewire_loop); g_source_attach (source, NULL); return source; } static void on_core_error (void *user_data, uint32_t id, int seq, int res, const char *message) { g_error ("PipeWire core error: id:%u %s", id, message); } static const struct pw_core_events core_events = { PW_VERSION_CORE_EVENTS, .error = on_core_error, }; void init_pipewire (void) { struct pw_loop *pipewire_loop; pw_init (NULL, NULL); pipewire_loop = pw_loop_new (NULL); g_assert_nonnull (pipewire_loop); _pipewire_source = create_pipewire_source (pipewire_loop); _pipewire_context = pw_context_new (pipewire_loop, NULL, 0); g_assert_nonnull (_pipewire_context); _pipewire_core = pw_context_connect (_pipewire_context, NULL, 0); g_assert_nonnull (_pipewire_core); pw_core_add_listener (_pipewire_core, &_pipewire_core_listener, &core_events, NULL); } void release_pipewire (void) { g_clear_pointer (&_pipewire_core, pw_core_disconnect); g_clear_pointer (&_pipewire_context, pw_context_destroy); if (_pipewire_source) { g_source_destroy (_pipewire_source); g_source_unref (_pipewire_source); _pipewire_source = NULL; } } static void on_stream_state_changed (void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { Stream *stream = user_data; g_debug ("New PipeWire stream (%u) state '%s'", stream->pipewire_node_id, pw_stream_state_as_string (state)); switch (state) { case PW_STREAM_STATE_ERROR: g_warning ("PipeWire stream error: %s", error); break; case PW_STREAM_STATE_PAUSED: case PW_STREAM_STATE_STREAMING: case PW_STREAM_STATE_UNCONNECTED: case PW_STREAM_STATE_CONNECTING: break; } stream->state = state; } static void on_stream_param_changed (void *user_data, uint32_t id, const struct spa_pod *format) { Stream *stream = user_data; uint8_t params_buffer[1024]; struct spa_pod_builder pod_builder; const struct spa_pod *params[3]; if (!format || id != SPA_PARAM_Format) return; spa_format_video_raw_parse (format, &stream->spa_format); pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); params[0] = spa_pod_builder_add_object ( &pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int (8, 1, 8), SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (1 << SPA_DATA_MemFd), 0); params[1] = spa_pod_builder_add_object ( &pod_builder, 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)), 0); params[2] = spa_pod_builder_add_object ( &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int (CURSOR_META_SIZE (384, 384), CURSOR_META_SIZE (1, 1), CURSOR_META_SIZE (384, 384)), 0); pw_stream_update_params (stream->pipewire_stream, params, G_N_ELEMENTS (params)); } static void process_buffer_metadata (Stream *stream, struct spa_buffer *buffer) { struct spa_meta_cursor *spa_meta_cursor; spa_meta_cursor = spa_buffer_find_meta_data (buffer, SPA_META_Cursor, sizeof *spa_meta_cursor); if (!spa_meta_cursor) return; if (!spa_meta_cursor_is_valid (spa_meta_cursor)) return; stream->cursor_x = spa_meta_cursor->position.x; stream->cursor_y = spa_meta_cursor->position.y; } static void sanity_check_memfd (struct spa_buffer *buffer) { size_t size; size = buffer->datas[0].maxsize + buffer->datas[0].mapoffset; g_assert_cmpint (size, >, 0); g_assert_nonnull (buffer->datas[0].data); } static void process_memfd_buffer (Stream *stream, struct pw_buffer *buffer) { sanity_check_memfd (buffer->buffer); if (stream->buffer) pw_stream_queue_buffer (stream->pipewire_stream, stream->buffer); stream->buffer = buffer; } static void process_buffer (Stream *stream, struct pw_buffer *buffer) { struct spa_buffer *spa_buffer = buffer->buffer; process_buffer_metadata (stream, buffer->buffer); if (spa_buffer->datas[0].chunk->size != 0) { if (spa_buffer->datas[0].type == SPA_DATA_MemFd) process_memfd_buffer (stream, buffer); else g_assert_not_reached (); } } static void on_stream_process (void *user_data) { Stream *stream = user_data; struct pw_buffer *next_buffer; struct pw_buffer *buffer = NULL; if (!stream->pipewire_stream) return; next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream); if (next_buffer) g_debug ("Dequeued buffer, queue previous"); while (next_buffer) { buffer = next_buffer; next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream); if (next_buffer) { g_debug ("Dequeued another buffer, queuing previous"); pw_stream_queue_buffer (stream->pipewire_stream, buffer); } } if (!buffer) return; process_buffer (stream, buffer); pw_stream_queue_buffer (stream->pipewire_stream, buffer); stream->buffer_count++; } static const struct pw_stream_events stream_events = { PW_VERSION_STREAM_EVENTS, .state_changed = on_stream_state_changed, .param_changed = on_stream_param_changed, .process = on_stream_process, }; static void stream_connect (Stream *stream) { struct pw_stream *pipewire_stream; uint8_t params_buffer[1024]; struct spa_pod_builder pod_builder; struct spa_rectangle rect; struct spa_rectangle min_rect; struct spa_rectangle max_rect; struct spa_fraction min_framerate; struct spa_fraction max_framerate; const struct spa_pod *params[2]; int ret; pipewire_stream = pw_stream_new (_pipewire_core, "mutter-test-pipewire-stream", NULL); switch (stream->stream_type) { case STREAM_TYPE_VIRTUAL: rect = SPA_RECTANGLE (stream->virtual.target_width, stream->virtual.target_height); min_framerate = SPA_FRACTION (1, 1); max_framerate = SPA_FRACTION (30, 1); pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); params[0] = spa_pod_builder_add_object ( &pod_builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, 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_VIDEO_FORMAT_BGRx), SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle (&rect), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction (&SPA_FRACTION(0, 1)), SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction (&min_framerate, &min_framerate, &max_framerate), 0); break; case STREAM_TYPE_MONITOR: min_rect = SPA_RECTANGLE (1, 1); max_rect = SPA_RECTANGLE (INT32_MAX, INT32_MAX); min_framerate = SPA_FRACTION (1, 1); max_framerate = SPA_FRACTION (30, 1); pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); params[0] = spa_pod_builder_add_object ( &pod_builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, 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_VIDEO_FORMAT_BGRx), SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle (&min_rect, &min_rect, &max_rect), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction (&SPA_FRACTION(0, 1)), SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction (&min_framerate, &min_framerate, &max_framerate), 0); break; } stream->pipewire_stream = pipewire_stream; pw_stream_add_listener (pipewire_stream, &stream->pipewire_stream_listener, &stream_events, stream); ret = pw_stream_connect (stream->pipewire_stream, PW_DIRECTION_INPUT, stream->pipewire_node_id, (PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params, 1); if (ret < 0) g_error ("Failed to connect PipeWire stream: %s", g_strerror (-ret)); } void stream_resize (Stream *stream, int width, int height) { uint8_t params_buffer[1024]; struct spa_pod_builder pod_builder; const struct spa_pod *params[1]; struct spa_rectangle rect; stream->virtual.target_width = width; stream->virtual.target_height = height; rect = SPA_RECTANGLE (width, height); pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); params[0] = spa_pod_builder_add_object ( &pod_builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle (&rect), 0); pw_stream_update_params (stream->pipewire_stream, params, G_N_ELEMENTS (params)); } void stream_wait_for_render (Stream *stream) { int initial_buffer_count = stream->buffer_count; g_debug ("Waiting for new buffer"); while (stream->buffer_count == initial_buffer_count) g_main_context_iteration (NULL, TRUE); } static void on_pipewire_stream_added (MetaDBusScreenCastStream *proxy, unsigned int node_id, Stream *stream) { stream->pipewire_node_id = (uint32_t) node_id; stream_connect (stream); } static Stream * stream_new_virtual (const char *path, int width, int height, CursorMode cursor_mode) { Stream *stream; GError *error = NULL; stream = g_new0 (Stream, 1); stream->stream_type = STREAM_TYPE_VIRTUAL; stream->cursor_mode = cursor_mode; stream->virtual.target_width = width; stream->virtual.target_height = height; stream->proxy = meta_dbus_screen_cast_stream_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.ScreenCast", path, NULL, &error); if (!stream->proxy) g_error ("Failed to acquire proxy: %s", error->message); g_signal_connect (stream->proxy, "pipewire-stream-added", G_CALLBACK (on_pipewire_stream_added), stream); return stream; } static Stream * stream_new_monitor (const char *path, CursorMode cursor_mode) { Stream *stream; GError *error = NULL; stream = g_new0 (Stream, 1); stream->stream_type = STREAM_TYPE_MONITOR; stream->cursor_mode = cursor_mode; stream->proxy = meta_dbus_screen_cast_stream_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.ScreenCast", path, NULL, &error); if (!stream->proxy) g_error ("Failed to acquire proxy: %s", error->message); g_signal_connect (stream->proxy, "pipewire-stream-added", G_CALLBACK (on_pipewire_stream_added), stream); return stream; } void stream_free (Stream *stream) { g_clear_pointer (&stream->pipewire_stream, pw_stream_destroy); g_clear_object (&stream->proxy); g_free (stream); } void session_notify_absolute_pointer (Session *session, Stream *stream, double x, double y) { GError *error = NULL; if (!meta_dbus_remote_desktop_session_call_notify_pointer_motion_absolute_sync ( session->remote_desktop_session_proxy, g_dbus_proxy_get_object_path (G_DBUS_PROXY (stream->proxy)), x, y, NULL, &error)) g_error ("Failed to send absolute pointer motion event: %s", error->message); } void session_start (Session *session) { GError *error = NULL; if (session->remote_desktop_session_proxy) { if (!meta_dbus_remote_desktop_session_call_start_sync ( session->remote_desktop_session_proxy, NULL, &error)) g_error ("Failed to start session: %s", error->message); } else { if (!meta_dbus_screen_cast_session_call_start_sync ( session->screen_cast_session_proxy, NULL, &error)) g_error ("Failed to start session: %s", error->message); } } void session_stop (Session *session) { GError *error = NULL; if (session->remote_desktop_session_proxy) { if (!meta_dbus_remote_desktop_session_call_stop_sync ( session->remote_desktop_session_proxy, NULL, &error)) g_error ("Failed to stop session: %s", error->message); } else { if (!meta_dbus_screen_cast_session_call_stop_sync ( session->screen_cast_session_proxy, NULL, &error)) g_error ("Failed to stop session: %s", error->message); } } Stream * session_record_virtual (Session *session, int width, int height, CursorMode cursor_mode) { GVariantBuilder properties_builder; GVariant *properties_variant; GError *error = NULL; g_autofree char *stream_path = NULL; Stream *stream; g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); g_variant_builder_add (&properties_builder, "{sv}", "cursor-mode", g_variant_new_uint32 (cursor_mode)); properties_variant = g_variant_builder_end (&properties_builder); if (!meta_dbus_screen_cast_session_call_record_virtual_sync ( session->screen_cast_session_proxy, properties_variant, &stream_path, NULL, &error)) g_error ("Failed to create session: %s", error->message); stream = stream_new_virtual (stream_path, width, height, cursor_mode); g_assert_nonnull (stream); return stream; } Stream * session_record_monitor (Session *session, const char *connector, CursorMode cursor_mode) { GVariantBuilder properties_builder; GVariant *properties_variant; GError *error = NULL; g_autofree char *stream_path = NULL; Stream *stream; g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); g_variant_builder_add (&properties_builder, "{sv}", "cursor-mode", g_variant_new_uint32 (cursor_mode)); properties_variant = g_variant_builder_end (&properties_builder); if (!meta_dbus_screen_cast_session_call_record_monitor_sync ( session->screen_cast_session_proxy, connector ? connector : "", properties_variant, &stream_path, NULL, &error)) g_error ("Failed to create session: %s", error->message); stream = stream_new_monitor (stream_path, cursor_mode); g_assert_nonnull (stream); return stream; } Session * session_new (MetaDBusRemoteDesktopSession *remote_desktop_session_proxy, MetaDBusScreenCastSession *screen_cast_session_proxy) { Session *session; session = g_new0 (Session, 1); session->remote_desktop_session_proxy = remote_desktop_session_proxy; session->screen_cast_session_proxy = screen_cast_session_proxy; return session; } void session_free (Session *session) { g_clear_object (&session->screen_cast_session_proxy); g_clear_object (&session->remote_desktop_session_proxy); g_free (session); } Session * screen_cast_create_session (RemoteDesktop *remote_desktop, ScreenCast *screen_cast) { GVariantBuilder properties_builder; GError *error = NULL; g_autofree char *remote_desktop_session_path = NULL; MetaDBusRemoteDesktopSession *remote_desktop_session_proxy = NULL; g_autofree char *screen_cast_session_path = NULL; MetaDBusScreenCastSession *screen_cast_session_proxy; const char *session_id = NULL; Session *session; if (remote_desktop) { if (!meta_dbus_remote_desktop_call_create_session_sync ( remote_desktop->proxy, &remote_desktop_session_path, NULL, &error)) g_error ("Failed to create session: %s", error->message); remote_desktop_session_proxy = meta_dbus_remote_desktop_session_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.RemoteDesktop", remote_desktop_session_path, NULL, &error); if (!remote_desktop_session_proxy) g_error ("Failed to acquire proxy: %s", error->message); session_id = meta_dbus_remote_desktop_session_get_session_id ( remote_desktop_session_proxy); } g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); if (session_id) { g_variant_builder_add (&properties_builder, "{sv}", "remote-desktop-session-id", g_variant_new_string (session_id)); } if (!meta_dbus_screen_cast_call_create_session_sync ( screen_cast->proxy, g_variant_builder_end (&properties_builder), &screen_cast_session_path, NULL, &error)) g_error ("Failed to create session: %s", error->message); screen_cast_session_proxy = meta_dbus_screen_cast_session_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.ScreenCast", screen_cast_session_path, NULL, &error); if (!screen_cast_session_proxy) g_error ("Failed to acquire proxy: %s", error->message); session = session_new (remote_desktop_session_proxy, screen_cast_session_proxy); g_assert_nonnull (session); return session; } RemoteDesktop * remote_desktop_new (void) { RemoteDesktop *remote_desktop; GError *error = NULL; remote_desktop = g_new0 (RemoteDesktop, 1); remote_desktop->proxy = meta_dbus_remote_desktop_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.RemoteDesktop", "/org/gnome/Mutter/RemoteDesktop", NULL, &error); if (!remote_desktop->proxy) g_error ("Failed to acquire proxy: %s", error->message); return remote_desktop; } void remote_desktop_free (RemoteDesktop *remote_desktop) { g_clear_object (&remote_desktop->proxy); g_free (remote_desktop); } ScreenCast * screen_cast_new (void) { ScreenCast *screen_cast; GError *error = NULL; screen_cast = g_new0 (ScreenCast, 1); screen_cast->proxy = meta_dbus_screen_cast_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.ScreenCast", "/org/gnome/Mutter/ScreenCast", NULL, &error); if (!screen_cast->proxy) g_error ("Failed to acquire proxy: %s", error->message); return screen_cast; } void screen_cast_free (ScreenCast *screen_cast) { g_clear_object (&screen_cast->proxy); g_free (screen_cast); }