Commit 053a68ba authored by Benjamin Berg's avatar Benjamin Berg
Browse files

Add pulseaudio source handling

For now this adds a ScreencastPulseaudio class which creates a
gnome_screencast NULL sink and allows gettign a pipewire source for the
monitor source.

The NULL sink is currently not destroyed on shutdown, however, we do
make sure to only ever create one of them.
parent 1b4b50ef
......@@ -28,6 +28,7 @@
#include <gst/gst.h>
#include "screencast-portal.h"
#include "screencast-pulseaudio.h"
struct _GnomeScreencastWindow
{
......@@ -39,6 +40,8 @@ struct _GnomeScreencastWindow
ScreencastPortal *portal;
gboolean use_x11;
ScreencastPulseaudio *pulse;
GCancellable *cancellable;
ScreencastSink *stream_sink;
......@@ -101,6 +104,19 @@ sink_create_source_cb (GnomeScreencastWindow * self, ScreencastSink * sink)
return GST_ELEMENT (bin);
}
static GstElement *
sink_create_audio_source_cb (GnomeScreencastWindow * self, ScreencastSink * sink)
{
GstElement *res;
if (!self->pulse)
return NULL;
res = screencast_pulseaudio_get_source (self->pulse);
return res;
}
static void
remove_widget (GtkWidget *widget, gpointer user_data)
{
......@@ -193,6 +209,12 @@ find_sink_list_row_activated_cb (GnomeScreencastWindow *self, ScreencastSinkRow
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->stream_sink,
"create-audio-source",
(GCallback) sink_create_audio_source_cb,
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->stream_sink,
"notify::state",
(GCallback) sink_notify_state_cb,
......@@ -275,6 +297,28 @@ screencast_portal_init_async_cb (GObject *source_object,
window->portal = SCREENCAST_PORTAL (source_object);
}
static void
screencast_pulseaudio_init_async_cb (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GnomeScreencastWindow *window;
g_autoptr(GError) error = NULL;
if (!g_async_initable_init_finish (G_ASYNC_INITABLE (source_object), res, &error))
{
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
g_warning ("Error initializing pulse audio sink: %s", error->message);
g_object_unref (source_object);
return;
}
window = GNOME_SCREENCAST_WINDOW (user_data);
window->pulse = SCREENCAST_PULSEAUDIO (source_object);
}
static void
stream_stop_clicked_cb (GnomeScreencastWindow *self)
{
......@@ -288,6 +332,8 @@ static void
gnome_screencast_window_init (GnomeScreencastWindow *self)
{
ScreencastPortal *portal;
ScreencastPulseaudio *pulse;
gtk_widget_init_template (GTK_WIDGET (self));
self->meta_provider = screencast_meta_provider_new ();
......@@ -337,4 +383,11 @@ gnome_screencast_window_init (GnomeScreencastWindow *self)
self->cancellable,
screencast_portal_init_async_cb,
self);
pulse = screencast_pulseaudio_new ();
g_async_initable_init_async (G_ASYNC_INITABLE (pulse),
G_PRIORITY_LOW,
self->cancellable,
screencast_pulseaudio_init_async_cb,
self);
}
......@@ -16,6 +16,7 @@ gnome_screencast_sources = [
'screencast-dummy-provider.c',
'screencast-dummy-wfd-sink.c',
'screencast-portal.c',
'screencast-pulseaudio.c',
]
enum_headers = files('screencast-sink.h')
......@@ -30,6 +31,7 @@ gnome_screencast_deps = [
dependency('gtk+-3.0', version: '>= 3.22'),
dependency('libnm', version: '>= 1.15'),
dependency('gstreamer-1.0', version: '>= 1.14'),
dependency('libpulse-mainloop-glib'),
]
gnome_screencast_deps += wfd_server_deps
......
......@@ -181,6 +181,16 @@ server_create_source_cb (ScreencastDummyWFDSink *sink, WfdServer *server)
return res;
}
static GstElement *
server_create_audio_source_cb (ScreencastDummyWFDSink *sink, WfdServer *server)
{
GstElement *res;
g_signal_emit_by_name (sink, "create-audio-source", &res);
return res;
}
static ScreencastSink *
screencast_dummy_wfd_sink_sink_start_stream (ScreencastSink *sink)
{
......@@ -216,6 +226,12 @@ screencast_dummy_wfd_sink_sink_start_stream (ScreencastSink *sink)
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->server,
"create-audio-source",
(GCallback) server_create_audio_source_cb,
self,
G_CONNECT_SWAPPED);
self->state = SCREENCAST_SINK_STATE_WAIT_SOCKET;
g_object_notify (G_OBJECT (self), "state");
......
#include "screencast-pulseaudio.h"
#include <pulse/glib-mainloop.h>
#include <pulse/pulseaudio.h>
G_DEFINE_AUTOPTR_CLEANUP_FUNC(pa_proplist, pa_proplist_free)
#define SCREENCAST_PA_SINK "gnome_screencast"
#define SCREENCAST_PA_MONITOR SCREENCAST_PA_SINK ".monitor"
struct _ScreencastPulseaudio
{
GObject parent_instance;
GTask *init_task;
pa_glib_mainloop *mainloop;
pa_mainloop_api *mainloop_api;
pa_context *context;
guint null_module_idx;
pa_operation *operation;
};
static void screencast_pulseaudio_async_initable_iface_init (GAsyncInitableIface *iface);
static void screencast_pulseaudio_async_initable_init_async (GAsyncInitable *initable,
int io_priority,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
static gboolean screencast_pulseaudio_async_initable_init_finish (GAsyncInitable *initable,
GAsyncResult *res,
GError **error);
G_DEFINE_TYPE_EXTENDED (ScreencastPulseaudio, screencast_pulseaudio, G_TYPE_OBJECT, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
screencast_pulseaudio_async_initable_iface_init);
)
enum {
PROP_0,
N_PROPS
};
static GParamSpec *properties [N_PROPS];
static void
screencast_pulseaudio_async_initable_iface_init (GAsyncInitableIface *iface)
{
iface->init_async = screencast_pulseaudio_async_initable_init_async;
iface->init_finish = screencast_pulseaudio_async_initable_init_finish;
}
static gboolean
return_idle_cb (gpointer user_data)
{
GTask *task = user_data;
GError *error = g_object_steal_data (G_OBJECT (task), "result");
if (error)
g_task_return_error (task, error);
else
g_task_return_boolean (task, TRUE);
g_object_unref (task);
return G_SOURCE_REMOVE;
}
/* Helper function to return an error from idle */
static void
return_idle_error (GTask *task, GError *error)
{
g_assert (error);
g_object_set_data_full (G_OBJECT (task), "result", error, (GDestroyNotify) g_error_free);
g_idle_add (return_idle_cb, g_object_ref (task));
}
static void
return_idle_success (GTask *task)
{
g_idle_add (return_idle_cb, g_object_ref (task));
}
static void
on_pa_null_module_loaded (pa_context *c,
uint32_t idx,
void *userdata)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (userdata);
if (idx == PA_INVALID_INDEX)
{
g_debug ("ScreencastPulseaudio: Module load failed!");
return_idle_error (self->init_task,
g_error_new (G_IO_ERROR,
G_IO_ERROR_FAILED,
"Failed to load NULL module for screencast PA sink"));
g_clear_object (&self->init_task);
return;
}
g_debug ("ScreencastPulseaudio: Module loaded, we are ready to grab audio! ");
self->null_module_idx = idx;
return_idle_success (self->init_task);
g_clear_object (&self->init_task);
}
static void
on_pa_screencast_sink_got_info (pa_context *c,
const pa_sink_info *i,
int eol,
void *userdata)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (userdata);
/* eol is negative for error, and 1 for end of list (with NULL sink info then) */
/* We are only ever called once, as we cancel the operation
* if we get what we want. */
if (eol < 0 && pa_context_errno (self->context) != PA_ERR_NOENTITY)
{
g_debug ("ScreencastPulseaudio: Error querying sink info");
return_idle_error (self->init_task,
g_error_new (G_IO_ERROR,
G_IO_ERROR_FAILED,
"Error querying sink from PA: %s",
pa_strerror (pa_context_errno (self->context))));
g_clear_object (&self->init_task);
return;
}
if (eol == 0)
{
/* This is the case that we could query the sink, so it seems
* like it exists already. Just double check things, and
* return successful initialization.
*/
/* Cancel the operation as we would be called a second time for
* the list end which would cause am immediate successfull return. */
pa_operation_cancel (self->operation);
g_clear_pointer (&self->operation, pa_operation_unref);
g_debug ("ScreencastPulseaudio: Error querying sink info");
g_debug ("ScreencastPulseaudio: Got a sink info for the expected name");
return_idle_success (self->init_task);
g_clear_object (&self->init_task);
return;
}
g_debug ("ScreencastPulseaudio: Sink does not exist yet, loading module");
/* We have reached the list end without being cancelled first.
* This means no screencast sink exist, and we need to create it. */
self->operation = pa_context_load_module (self->context,
"module-null-sink",
"sink_name=gnome_screencast "
"sink_properties=device.description=\"GNOME-Screencast\""
"device.class=\"sound\""
"device.icon_name=\"network-wireless\"",
on_pa_null_module_loaded,
self);
}
static void
screencast_pulseaudio_state_cb (pa_context *context,
void *user_data)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (user_data);
switch (pa_context_get_state (context))
{
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
if (!self->init_task)
return;
g_debug ("ScreencastPulseaudio: Querying sink info by name");
self->operation = pa_context_get_sink_info_by_name (self->context,
SCREENCAST_PA_SINK,
on_pa_screencast_sink_got_info,
self);
break;
case PA_CONTEXT_FAILED:
case PA_CONTEXT_TERMINATED:
if (!self->init_task)
return;
g_debug ("ScreencastPulseaudio: PA context went into failed state during init");
return_idle_error (self->init_task,
g_error_new (G_IO_ERROR,
G_IO_ERROR_FAILED,
"PA failed"));
g_clear_object (&self->init_task);
break;
default:
/* FIXME: */
break;
}
}
static void
screencast_pulseaudio_async_initable_init_async (GAsyncInitable *initable,
int io_priority,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (initable);
g_autoptr(pa_proplist) proplist = NULL;
gint res;
self->mainloop = pa_glib_mainloop_new (g_main_context_default ());
self->mainloop_api = pa_glib_mainloop_get_api (self->mainloop);
proplist = pa_proplist_new ();
pa_proplist_sets (proplist, PA_PROP_APPLICATION_NAME, "GNOME Screencast");
pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "org.gnome.Screencast");
/* pa_proplist_sets (proplist, PA_PROP_APPLICATION_ICON_NAME, ); */
self->context = pa_context_new_with_proplist (self->mainloop_api, NULL, proplist);
/* Create our task; we currently don't handle cancellation internally */
self->init_task = g_task_new (initable, cancellable, callback, user_data);
pa_context_set_state_callback (self->context,
screencast_pulseaudio_state_cb,
self);
res = pa_context_connect (self->context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFLAGS, NULL);
if (res < 0)
{
g_debug ("ScreencastPulseaudio: Error querying sink info");
g_task_return_new_error (self->init_task,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Error connecting to PA: %s",
pa_strerror (pa_context_errno (self->context)));
g_clear_object (&self->init_task);
return;
}
/* Wait for us to be connected. */
}
static gboolean
screencast_pulseaudio_async_initable_init_finish (GAsyncInitable *initable,
GAsyncResult *res,
GError **error)
{
return g_task_propagate_boolean (G_TASK (res), error);
}
ScreencastPulseaudio *
screencast_pulseaudio_new (void)
{
return g_object_new (SCREENCAST_TYPE_PULSEAUDIO, NULL);
}
static void
screencast_pulseaudio_finalize (GObject *object)
{
ScreencastPulseaudio *self = (ScreencastPulseaudio *)object;
if (self->init_task)
{
g_task_return_new_error (self->init_task,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"Object finalised, async init was cancelled.");
g_clear_object (&self->init_task);
}
if (self->operation)
pa_operation_cancel (self->operation);
g_clear_pointer (&self->operation, pa_operation_unref);
g_clear_pointer (&self->context, pa_context_unref);
self->mainloop_api = NULL;
g_clear_pointer (&self->mainloop, pa_glib_mainloop_free);
G_OBJECT_CLASS (screencast_pulseaudio_parent_class)->finalize (object);
}
static void
screencast_pulseaudio_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (object);
switch (prop_id)
{
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
screencast_pulseaudio_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ScreencastPulseaudio *self = SCREENCAST_PULSEAUDIO (object);
switch (prop_id)
{
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
screencast_pulseaudio_class_init (ScreencastPulseaudioClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = screencast_pulseaudio_finalize;
object_class->get_property = screencast_pulseaudio_get_property;
object_class->set_property = screencast_pulseaudio_set_property;
}
static void
screencast_pulseaudio_init (ScreencastPulseaudio *self)
{
self->null_module_idx = PA_INVALID_INDEX;
}
GstElement*
screencast_pulseaudio_get_source (ScreencastPulseaudio *self)
{
g_autoptr(GstElement) src = NULL;
g_assert (self->init_task == NULL);
g_assert (self->context != NULL);
src = gst_element_factory_make ("pulsesrc", "pulseaudio-source");
g_object_set (src,
"device", SCREENCAST_PA_MONITOR,
"client-name", "GNOME-Screencast Audio Grabber",
"do-timestamp", TRUE,
"server", pa_context_get_server (self->context),
NULL);
return g_steal_pointer (&src);
}
#pragma once
#include <gio/gio.h>
#include <glib-object.h>
#include <gst/gst.h>
G_BEGIN_DECLS
#define SCREENCAST_TYPE_PULSEAUDIO (screencast_pulseaudio_get_type())
G_DECLARE_FINAL_TYPE (ScreencastPulseaudio, screencast_pulseaudio, SCREENCAST, PULSEAUDIO, GObject)
ScreencastPulseaudio *screencast_pulseaudio_new (void);
GstElement *screencast_pulseaudio_get_source (ScreencastPulseaudio *self);
G_END_DECLS
......@@ -34,6 +34,12 @@ screencast_sink_default_init (ScreencastSinkIface *iface)
NULL,
GST_TYPE_ELEMENT, 0);
g_signal_new ("create-audio-source", SCREENCAST_TYPE_SINK, G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_first_wins, NULL,
NULL,
GST_TYPE_ELEMENT, 0);
g_object_interface_install_property (iface,
g_param_spec_string ("display-name",
"Display Name",
......
......@@ -306,6 +306,16 @@ server_create_source_cb (ScreencastWFDP2PSink *sink, WfdServer *server)
return res;
}
static GstElement *
server_create_audio_source_cb (ScreencastWFDP2PSink *sink, WfdServer *server)
{
GstElement *res;
g_signal_emit_by_name (sink, "create-audio-source", &res);
return res;
}
static void
p2p_connected (GObject *source_object,
GAsyncResult *res,
......@@ -364,6 +374,12 @@ p2p_connected (GObject *source_object,
sink,
G_CONNECT_SWAPPED);
g_signal_connect_object (sink->server,
"create-audio-source",
(GCallback) server_create_audio_source_cb,
sink,
G_CONNECT_SWAPPED);
sink->state = SCREENCAST_SINK_STATE_WAIT_SOCKET;
g_object_notify (G_OBJECT (sink), "state");
}
......
......@@ -26,6 +26,7 @@ static GParamSpec *properties[N_PROPS];
enum {
SIGNAL_CREATE_SOURCE,
SIGNAL_CREATE_AUDIO_SOURCE,
NR_SIGNALS
};
......@@ -455,6 +456,13 @@ wfd_media_factory_class_init (WfdMediaFactoryClass *klass)
g_signal_accumulator_first_wins, NULL,
NULL,
GST_TYPE_ELEMENT, 0);
signals[SIGNAL_CREATE_AUDIO_SOURCE] =
g_signal_new ("create-audio-source", WFD_TYPE_MEDIA_FACTORY, G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_first_wins, NULL,
NULL,
GST_TYPE_ELEMENT, 0);
}
static void
......
......@@ -20,6 +20,7 @@ static GParamSpec *properties[N_PROPS];
enum {
SIGNAL_CREATE_SOURCE,
SIGNAL_CREATE_AUDIO_SOURCE,
NR_SIGNALS
};
......@@ -163,6 +164,13 @@ wfd_server_class_init (WfdServerClass *klass)
g_signal_accumulator_first_wins, NULL,
NULL,
GST_TYPE_ELEMENT, 0);
signals[SIGNAL_CREATE_AUDIO_SOURCE] =
g_signal_new ("create-audio-source", WFD_TYPE_SERVER, G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_first_wins, NULL,
NULL,
GST_TYPE_ELEMENT, 0);
}
static gboolean
......@@ -188,6 +196,16 @@ factory_source_create_cb (WfdMediaFactory *factory, WfdServer *self)
return res;
}
static GstElement *
factory_audio_source_create_cb (WfdMediaFactory *factory, WfdServer *self)
{
GstElement *res;
g_signal_emit (self, signals[SIGNAL_CREATE_AUDIO_SOURCE], 0, &res);
return res;
}
static void