From 1940cd7fe9563c1f7ebc413adca08708630aeea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20=C3=85dahl?= Date: Mon, 1 Feb 2021 20:32:27 +0100 Subject: [PATCH] tests: Add tests for virtual screen cast source Tests that creating and starting a virtual screen cast monitor works, and that at least one one buffer is processed. Currently the content of the buffer isn't checked more than it can be mmap():ed. Only MemFd buffers are tested for for now, as DMA buffers would need a surfaceless EGL context to check properly. Part-of: --- meson.build | 3 + src/tests/meson.build | 17 + src/tests/native-headless.c | 2 + src/tests/native-screen-cast.c | 86 +++++ src/tests/native-screen-cast.h | 26 ++ src/tests/screen-cast-client.c | 572 +++++++++++++++++++++++++++++++++ 6 files changed, 706 insertions(+) create mode 100644 src/tests/native-screen-cast.c create mode 100644 src/tests/native-screen-cast.h create mode 100644 src/tests/screen-cast-client.c diff --git a/meson.build b/meson.build index c4bfdfe25..9dd8c5e2a 100644 --- a/meson.build +++ b/meson.build @@ -277,6 +277,9 @@ if have_tests if not have_native_backend error('Native tests require the native backend') endif + if not have_remote_desktop + error('Native tests require remote desktop') + endif endif have_cogl_tests = get_option('cogl_tests') diff --git a/src/tests/meson.build b/src/tests/meson.build index e570db8c6..8ffe2f9e1 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -166,6 +166,8 @@ if have_native_tests native_headless_tests = executable('mutter-native-headless-tests', sources: [ 'native-headless.c', + 'native-screen-cast.c', + 'native-screen-cast.h', 'native-virtual-monitor.c', 'native-virtual-monitor.h', 'test-utils.c', @@ -192,6 +194,21 @@ if have_native_tests install: have_installed_tests, install_dir: mutter_installed_tests_libexecdir, ) + + screen_cast_client = executable('mutter-screen-cast-client', + sources: [ + 'screen-cast-client.c', + dbus_screen_cast_built_sources, + ], + include_directories: tests_includepath, + c_args: tests_c_args, + dependencies: [ + gio_dep, + libpipewire_dep, + ], + install: have_installed_tests, + install_dir: mutter_installed_tests_libexecdir, + ) endif stacking_tests = [ diff --git a/src/tests/native-headless.c b/src/tests/native-headless.c index 1b3d4a26c..6b5da665c 100644 --- a/src/tests/native-headless.c +++ b/src/tests/native-headless.c @@ -26,6 +26,7 @@ #include "core/main-private.h" #include "meta/main.h" #include "meta/meta-backend.h" +#include "tests/native-screen-cast.h" #include "tests/native-virtual-monitor.h" #include "tests/test-utils.h" @@ -33,6 +34,7 @@ static void init_tests (void) { init_virtual_monitor_tests (); + init_screen_cast_tests (); } static gboolean diff --git a/src/tests/native-screen-cast.c b/src/tests/native-screen-cast.c new file mode 100644 index 000000000..bcde15665 --- /dev/null +++ b/src/tests/native-screen-cast.c @@ -0,0 +1,86 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "tests/native-screen-cast.h" + +#include +#include +#include + +static void +test_client_exited (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + + if (!g_subprocess_wait_finish (G_SUBPROCESS (source_object), + result, + &error)) + g_error ("Screen cast test client exited with an error: %s", error->message); + + g_main_loop_quit (user_data); +} + +static void +meta_test_screen_cast_record_virtual (void) +{ + GSubprocessLauncher *launcher; + g_autofree char *test_client_path = NULL; + GError *error = NULL; + GSubprocess *subprocess; + GMainLoop *loop; + + launcher = g_subprocess_launcher_new ((G_SUBPROCESS_FLAGS_STDIN_PIPE | + G_SUBPROCESS_FLAGS_STDOUT_PIPE)); + + test_client_path = g_test_build_filename (G_TEST_BUILT, + "src", + "tests", + "mutter-screen-cast-client", + NULL); + g_subprocess_launcher_setenv (launcher, + "XDG_RUNTIME_DIR", getenv ("XDG_RUNTIME_DIR"), + TRUE); + subprocess = g_subprocess_launcher_spawn (launcher, + &error, + test_client_path, + NULL); + if (!subprocess) + g_error ("Failed to launch screen cast test client: %s", error->message); + + loop = g_main_loop_new (NULL, FALSE); + g_subprocess_wait_check_async (subprocess, + NULL, + test_client_exited, + loop); + g_main_loop_run (loop); + g_assert_true (g_subprocess_get_successful (subprocess)); + g_object_unref (subprocess); +} + +void +init_screen_cast_tests (void) +{ + g_test_add_func ("/backends/native/screen-cast/record-virtual", + meta_test_screen_cast_record_virtual); +} diff --git a/src/tests/native-screen-cast.h b/src/tests/native-screen-cast.h new file mode 100644 index 000000000..3c79676c7 --- /dev/null +++ b/src/tests/native-screen-cast.h @@ -0,0 +1,26 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#ifndef NATIVE_SCREEN_CAST_H +#define NATIVE_SCREEN_CAST_H + +void init_screen_cast_tests (void); + +#endif /* NATIVE_SCREEN_CAST_H */ diff --git a/src/tests/screen-cast-client.c b/src/tests/screen-cast-client.c new file mode 100644 index 000000000..cc118f7ae --- /dev/null +++ b/src/tests/screen-cast-client.c @@ -0,0 +1,572 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "meta-dbus-screen-cast.h" + +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; +} Stream; + +typedef struct _Session +{ + MetaDBusScreenCastSession *proxy; +} Session; + +typedef struct _ScreenCast +{ + MetaDBusScreenCast *proxy; +} ScreenCast; + +typedef struct _PipeWireSource +{ + GSource base; + + struct pw_loop *pipewire_loop; +} PipeWireSource; + +static PipeWireSource *_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 *base, + 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 PipeWireSource * +create_pipewire_source (void) +{ + PipeWireSource *pipewire_source; + + pipewire_source = + (PipeWireSource *) g_source_new (&pipewire_source_funcs, + sizeof (PipeWireSource)); + pipewire_source->pipewire_loop = pw_loop_new (NULL); + g_assert_nonnull (pipewire_source->pipewire_loop); + g_source_add_unix_fd (&pipewire_source->base, + pw_loop_get_fd (pipewire_source->pipewire_loop), + G_IO_IN | G_IO_ERR); + + pw_loop_enter (pipewire_source->pipewire_loop); + g_source_attach (&pipewire_source->base, NULL); + + return pipewire_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) +{ + pw_init (NULL, NULL); + _pipewire_source = create_pipewire_source (); + _pipewire_context = pw_context_new (_pipewire_source->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 ((GSource *) _pipewire_source); + g_source_unref ((GSource *) _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; + + 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[2]; + + 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); + + pw_stream_update_params (stream->pipewire_stream, + params, G_N_ELEMENTS (params)); +} + +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) +{ + 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; + + next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream); + while (next_buffer) + { + buffer = next_buffer; + next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream); + + if (next_buffer) + 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 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); + + min_rect = SPA_RECTANGLE (1, 1); + max_rect = SPA_RECTANGLE (50 , 50); + 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); + + 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 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) +{ + while (stream->buffer_count == 0) + 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 (const char *path) +{ + Stream *stream; + GError *error = NULL; + + stream = g_new0 (Stream, 1); + 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_start (Session *session) +{ + GError *error = NULL; + + if (!meta_dbus_screen_cast_session_call_start_sync (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_screen_cast_session_call_stop_sync (session->proxy, + NULL, + &error)) + g_error ("Failed to stop session: %s", error->message); +} + +static Stream * +session_record_virtual (Session *session) +{ + 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}")); + properties_variant = g_variant_builder_end (&properties_builder); + + if (!meta_dbus_screen_cast_session_call_record_virtual_sync ( + session->proxy, + properties_variant, + &stream_path, + NULL, + &error)) + g_error ("Failed to create session: %s", error->message); + + stream = stream_new (stream_path); + g_assert_nonnull (stream); + return stream; +} + +static Session * +session_new (const char *path) +{ + Session *session; + GError *error = NULL; + + session = g_new0 (Session, 1); + 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", + path, + NULL, + &error); + if (!session->proxy) + g_error ("Failed to acquire proxy: %s", error->message); + + return session; +} + +static void +session_free (Session *session) +{ + g_clear_object (&session->proxy); + g_free (session); +} + +static Session * +screen_cast_create_session (ScreenCast *screen_cast) +{ + GVariantBuilder properties_builder; + GVariant *properties_variant; + GError *error = NULL; + g_autofree char *session_path = NULL; + Session *session; + + g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); + properties_variant = g_variant_builder_end (&properties_builder); + + if (!meta_dbus_screen_cast_call_create_session_sync (screen_cast->proxy, + properties_variant, + &session_path, + NULL, + &error)) + g_error ("Failed to create session: %s", error->message); + + session = session_new (session_path); + g_assert_nonnull (session); + return session; +} + +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) +{ + ScreenCast *screen_cast; + Session *session; + Stream *stream; + + init_pipewire (); + + screen_cast = screen_cast_new (); + session = screen_cast_create_session (screen_cast); + stream = session_record_virtual (session); + + session_start (session); + + stream_wait_for_node (stream); + stream_wait_for_streaming (stream); + stream_wait_for_render (stream); + + session_stop (session); + + stream_free (stream); + session_free (session); + screen_cast_free (screen_cast); + + release_pipewire (); + + return EXIT_SUCCESS; +}