/*
* Copyright (C) 2021 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
#include
#include
#include
#include
#include
#include
#include "meta-dbus-remote-desktop.h"
#include "meta-dbus-screen-cast.h"
#define assert_cursor_position(stream, x, y) \
{ \
g_assert_cmpint (stream->cursor_x, ==, (x)); \
g_assert_cmpint (stream->cursor_y, ==, (y)); \
}
#define CURSOR_META_SIZE(width, height) \
(sizeof(struct spa_meta_cursor) + \
sizeof(struct spa_meta_bitmap) + width * height * 4)
enum
{
CURSOR_MODE_HIDDEN = 0,
CURSOR_MODE_EMBEDDED = 1,
CURSOR_MODE_METADATA = 2,
};
typedef struct _Stream
{
MetaDBusScreenCastStream *proxy;
uint32_t pipewire_node_id;
struct spa_video_info_raw spa_format;
struct pw_stream *pipewire_stream;
struct spa_hook pipewire_stream_listener;
enum pw_stream_state state;
int buffer_count;
int target_width;
int target_height;
int cursor_x;
int cursor_y;
} Stream;
typedef struct _Session
{
MetaDBusScreenCastSession *screen_cast_session_proxy;
MetaDBusRemoteDesktopSession *remote_desktop_session_proxy;
} Session;
typedef struct _RemoteDesktop
{
MetaDBusRemoteDesktop *proxy;
} RemoteDesktop;
typedef struct _ScreenCast
{
MetaDBusScreenCast *proxy;
} ScreenCast;
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,
};
static 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);
}
static 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_MemPtr) |
(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;
uint8_t *map;
size = buffer->datas[0].maxsize + buffer->datas[0].mapoffset;
g_assert_cmpint (size, >, 0);
map = mmap (NULL, size, PROT_READ, MAP_PRIVATE, buffer->datas[0].fd, 0);
g_assert (map != MAP_FAILED);
munmap (map, size);
}
static void
sanity_check_memptr (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_buffer (Stream *stream,
struct spa_buffer *buffer)
{
process_buffer_metadata (stream, buffer);
if (buffer->datas[0].chunk->size == 0)
g_assert_not_reached ();
else if (buffer->datas[0].type == SPA_DATA_MemFd)
sanity_check_memfd (buffer);
else if (buffer->datas[0].type == SPA_DATA_DmaBuf)
g_assert_not_reached ();
else if (buffer->datas[0].type == SPA_DATA_MemPtr)
sanity_check_memptr (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->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_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);
rect = SPA_RECTANGLE (stream->target_width, stream->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);
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,
params, 1);
if (ret < 0)
g_error ("Failed to connect PipeWire stream: %s", g_strerror (-ret));
}
static void
stream_wait_for_node (Stream *stream)
{
while (!stream->pipewire_node_id)
g_main_context_iteration (NULL, TRUE);
}
static void
stream_wait_for_cursor_position (Stream *stream,
int x,
int y)
{
while (stream->cursor_x != x &&
stream->cursor_y != y)
g_main_context_iteration (NULL, TRUE);
}
static G_GNUC_UNUSED void
stream_wait_for_streaming (Stream *stream)
{
while (stream->state != PW_STREAM_STATE_STREAMING)
g_main_context_iteration (NULL, TRUE);
}
static G_GNUC_UNUSED void
stream_wait_for_render (Stream *stream)
{
int initial_buffer_count = stream->buffer_count;
while (stream->buffer_count == initial_buffer_count)
g_main_context_iteration (NULL, TRUE);
}
static 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->target_width = width;
stream->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));
}
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 (const char *path,
int width,
int height)
{
Stream *stream;
GError *error = NULL;
stream = g_new0 (Stream, 1);
stream->target_width = width;
stream->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 void
stream_free (Stream *stream)
{
g_clear_pointer (&stream->pipewire_stream, pw_stream_destroy);
g_clear_object (&stream->proxy);
g_free (stream);
}
static 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);
}
static void
session_start (Session *session)
{
GError *error = NULL;
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);
}
static void
session_stop (Session *session)
{
GError *error = NULL;
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);
}
static Stream *
session_record_virtual (Session *session,
int width,
int height)
{
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_METADATA));
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 (stream_path, width, height);
g_assert_nonnull (stream);
return stream;
}
static 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;
}
static void
session_free (Session *session)
{
g_clear_object (&session->screen_cast_session_proxy);
g_clear_object (&session->remote_desktop_session_proxy);
g_free (session);
}
static 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;
g_autofree char *screen_cast_session_path = NULL;
MetaDBusScreenCastSession *screen_cast_session_proxy;
const char *session_id;
Session *session;
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}"));
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;
}
static 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;
}
static void
remote_desktop_free (RemoteDesktop *remote_desktop)
{
g_clear_object (&remote_desktop->proxy);
g_free (remote_desktop);
}
static 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;
}
static void
screen_cast_free (ScreenCast *screen_cast)
{
g_clear_object (&screen_cast->proxy);
g_free (screen_cast);
}
int
main (int argc,
char **argv)
{
RemoteDesktop *remote_desktop;
ScreenCast *screen_cast;
Session *session;
Stream *stream;
g_debug ("Initializing PipeWire");
init_pipewire ();
g_debug ("Creating screen cast session");
remote_desktop = remote_desktop_new ();
screen_cast = screen_cast_new ();
session = screen_cast_create_session (remote_desktop, screen_cast);
stream = session_record_virtual (session, 50, 40);
g_debug ("Starting screen cast stream");
session_start (session);
/* Check that the display server handles events being emitted too early. */
session_notify_absolute_pointer (session, stream, 2, 3);
/* Check that we receive the initial frame */
g_debug ("Waiting for stream to be established");
stream_wait_for_node (stream);
stream_wait_for_render (stream);
stream_wait_for_streaming (stream);
session_notify_absolute_pointer (session, stream, 6, 5);
session_notify_absolute_pointer (session, stream, 5, 6);
g_debug ("Waiting for frame");
stream_wait_for_render (stream);
stream_wait_for_cursor_position (stream, 5, 6);
g_assert_cmpint (stream->spa_format.size.width, ==, 50);
g_assert_cmpint (stream->spa_format.size.height, ==, 40);
/* Check that resizing works */
g_debug ("Resizing stream");
stream_resize (stream, 70, 60);
while (TRUE)
{
stream_wait_for_render (stream);
if (stream->spa_format.size.width == 70 &&
stream->spa_format.size.height == 60)
break;
g_assert_cmpint (stream->spa_format.size.width, ==, 50);
g_assert_cmpint (stream->spa_format.size.height, ==, 40);
}
/* Check that resizing works */
stream_resize (stream, 60, 60);
g_debug ("Stopping session");
session_stop (session);
stream_free (stream);
session_free (session);
screen_cast_free (screen_cast);
remote_desktop_free (remote_desktop);
release_pipewire ();
g_debug ("Done");
return EXIT_SUCCESS;
}