summaryrefslogtreecommitdiff
path: root/lib/wireplumber/src/wireplumber.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/wireplumber/src/wireplumber.c')
-rw-r--r--lib/wireplumber/src/wireplumber.c503
1 files changed, 503 insertions, 0 deletions
diff --git a/lib/wireplumber/src/wireplumber.c b/lib/wireplumber/src/wireplumber.c
new file mode 100644
index 0000000..f1fa516
--- /dev/null
+++ b/lib/wireplumber/src/wireplumber.c
@@ -0,0 +1,503 @@
+#include <wp/wp.h>
+
+#include "audio.h"
+#include "device-private.h"
+#include "endpoint-private.h"
+#include "glib-object.h"
+#include "glib.h"
+#include "video.h"
+#include "wp.h"
+
+struct _AstalWpWp {
+ GObject parent_instance;
+
+ AstalWpEndpoint *default_speaker;
+ AstalWpEndpoint *default_microphone;
+
+ AstalWpAudio *audio;
+ AstalWpVideo *video;
+
+ AstalWpScale scale;
+};
+
+typedef struct {
+ WpCore *core;
+ WpObjectManager *obj_manager;
+
+ WpPlugin *mixer;
+ WpPlugin *defaults;
+ gint pending_plugins;
+
+ GHashTable *endpoints;
+ GHashTable *devices;
+} AstalWpWpPrivate;
+
+G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpWp, astal_wp_wp, G_TYPE_OBJECT);
+
+G_DEFINE_ENUM_TYPE(AstalWpScale, astal_wp_scale,
+ G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_LINEAR, "linear"),
+ G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_CUBIC, "cubic"));
+
+typedef enum {
+ ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED,
+ ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED,
+ ASTAL_WP_WP_SIGNAL_DEVICE_ADDED,
+ ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED,
+ ASTAL_WP_WP_N_SIGNALS
+} AstalWpWpSignals;
+
+static guint astal_wp_wp_signals[ASTAL_WP_WP_N_SIGNALS] = {
+ 0,
+};
+
+typedef enum {
+ ASTAL_WP_WP_PROP_AUDIO = 1,
+ ASTAL_WP_WP_PROP_VIDEO,
+ ASTAL_WP_WP_PROP_ENDPOINTS,
+ ASTAL_WP_WP_PROP_DEVICES,
+ ASTAL_WP_WP_PROP_DEFAULT_SPEAKER,
+ ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE,
+ ASTAL_WP_WP_PROP_SCALE,
+ ASTAL_WP_WP_N_PROPERTIES,
+} AstalWpWpProperties;
+
+static GParamSpec *astal_wp_wp_properties[ASTAL_WP_WP_N_PROPERTIES] = {
+ NULL,
+};
+
+/**
+ * astal_wp_wp_get_endpoint:
+ * @self: the AstalWpWp object
+ * @id: the id of the endpoint
+ *
+ * Returns: (transfer none) (nullable): the endpoint with the given id
+ */
+AstalWpEndpoint *astal_wp_wp_get_endpoint(AstalWpWp *self, guint id) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ AstalWpEndpoint *endpoint = g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id));
+ return endpoint;
+}
+
+/**
+ * astal_wp_wp_get_endpoints:
+ * @self: the AstalWpWp object
+ *
+ * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the
+ * endpoints
+ */
+GList *astal_wp_wp_get_endpoints(AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+ return g_hash_table_get_values(priv->endpoints);
+}
+
+/**
+ * astal_wp_wp_get_device:
+ * @self: the AstalWpWp object
+ * @id: the id of the device
+ *
+ * Returns: (transfer none) (nullable): the device with the given id
+ */
+AstalWpDevice *astal_wp_wp_get_device(AstalWpWp *self, guint id) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ AstalWpDevice *device = g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id));
+ return device;
+}
+
+/**
+ * astal_wp_wp_get_devices:
+ * @self: the AstalWpWp object
+ *
+ * Returns: (transfer container) (nullable) (type GList(AstalWpDevice)): a GList containing the
+ * devices
+ */
+GList *astal_wp_wp_get_devices(AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+ return g_hash_table_get_values(priv->devices);
+}
+
+/**
+ * astal_wp_wp_get_audio
+ *
+ * Returns: (nullable) (transfer none): gets the audio object
+ */
+AstalWpAudio *astal_wp_wp_get_audio(AstalWpWp *self) { return self->audio; }
+
+/**
+ * astal_wp_wp_get_video
+ *
+ * Returns: (nullable) (transfer none): gets the video object
+ */
+AstalWpVideo *astal_wp_wp_get_video(AstalWpWp *self) { return self->video; }
+
+/**
+ * astal_wp_wp_get_default_speaker
+ *
+ * Returns: (nullable) (transfer none): gets the default speaker object
+ */
+AstalWpEndpoint *astal_wp_wp_get_default_speaker(AstalWpWp *self) { return self->default_speaker; }
+
+/**
+ * astal_wp_wp_get_default_microphone
+ *
+ * Returns: (nullable) (transfer none): gets the default microphone object
+ */
+AstalWpEndpoint *astal_wp_wp_get_default_microphone(AstalWpWp *self) {
+ return self->default_microphone;
+}
+
+AstalWpScale astal_wp_wp_get_scale(AstalWpWp *self) { return self->scale; }
+
+void astal_wp_wp_set_scale(AstalWpWp *self, AstalWpScale scale) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+ self->scale = scale;
+
+ if (priv->mixer == NULL) return;
+
+ g_object_set(priv->mixer, "scale", self->scale, NULL);
+
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init(&iter, priv->endpoints);
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ AstalWpEndpoint *ep = ASTAL_WP_ENDPOINT(value);
+ astal_wp_endpoint_update_volume(ep);
+ }
+
+ astal_wp_endpoint_update_volume(self->default_speaker);
+ astal_wp_endpoint_update_volume(self->default_microphone);
+}
+
+static void astal_wp_wp_get_property(GObject *object, guint property_id, GValue *value,
+ GParamSpec *pspec) {
+ AstalWpWp *self = ASTAL_WP_WP(object);
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ switch (property_id) {
+ case ASTAL_WP_WP_PROP_AUDIO:
+ g_value_set_object(value, astal_wp_wp_get_audio(self));
+ break;
+ case ASTAL_WP_WP_PROP_VIDEO:
+ g_value_set_object(value, astal_wp_wp_get_video(self));
+ break;
+ case ASTAL_WP_WP_PROP_ENDPOINTS:
+ g_value_set_pointer(value, g_hash_table_get_values(priv->endpoints));
+ break;
+ case ASTAL_WP_WP_PROP_DEVICES:
+ g_value_set_pointer(value, g_hash_table_get_values(priv->devices));
+ break;
+ case ASTAL_WP_WP_PROP_DEFAULT_SPEAKER:
+ g_value_set_object(value, self->default_speaker);
+ break;
+ case ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE:
+ g_value_set_object(value, self->default_microphone);
+ break;
+ case ASTAL_WP_WP_PROP_SCALE:
+ g_value_set_enum(value, self->scale);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_wp_wp_set_property(GObject *object, guint property_id, const GValue *value,
+ GParamSpec *pspec) {
+ AstalWpWp *self = ASTAL_WP_WP(object);
+
+ switch (property_id) {
+ case ASTAL_WP_WP_PROP_SCALE:
+ astal_wp_wp_set_scale(self, g_value_get_enum(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_wp_wp_object_added(AstalWpWp *self, gpointer object) {
+ // print pipewire properties
+ // WpIterator *iter = wp_pipewire_object_new_properties_iterator(WP_PIPEWIRE_OBJECT(object));
+ // GValue item = G_VALUE_INIT;
+ // const gchar *key, *value;
+ //
+ // g_print("\n\n");
+ // while (wp_iterator_next (iter, &item)) {
+ // WpPropertiesItem *pi = g_value_get_boxed (&item);
+ // key = wp_properties_item_get_key (pi);
+ // value = wp_properties_item_get_value (pi);
+ // g_print("%s: %s\n", key, value);
+ // g_value_unset(&item);
+ // }
+
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ if (WP_IS_NODE(object)) {
+ WpNode *node = WP_NODE(object);
+ AstalWpEndpoint *endpoint =
+ astal_wp_endpoint_create(node, priv->mixer, priv->defaults, self);
+
+ g_hash_table_insert(priv->endpoints,
+ GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))), endpoint);
+
+ g_signal_emit_by_name(self, "endpoint-added", endpoint);
+ g_object_notify(G_OBJECT(self), "endpoints");
+ } else if (WP_IS_DEVICE(object)) {
+ WpDevice *node = WP_DEVICE(object);
+ AstalWpDevice *device = astal_wp_device_create(node);
+ g_hash_table_insert(priv->devices, GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))),
+ device);
+ g_signal_emit_by_name(self, "device-added", device);
+ g_object_notify(G_OBJECT(self), "devices");
+ }
+}
+
+static void astal_wp_wp_object_removed(AstalWpWp *self, gpointer object) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ if (WP_IS_NODE(object)) {
+ guint id = wp_proxy_get_bound_id(WP_PROXY(object));
+ AstalWpEndpoint *endpoint =
+ g_object_ref(g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id)));
+
+ g_hash_table_remove(priv->endpoints, GUINT_TO_POINTER(id));
+
+ g_signal_emit_by_name(self, "endpoint-removed", endpoint);
+ g_object_notify(G_OBJECT(self), "endpoints");
+ g_object_unref(endpoint);
+ } else if (WP_IS_DEVICE(object)) {
+ guint id = wp_proxy_get_bound_id(WP_PROXY(object));
+ AstalWpDevice *device =
+ g_object_ref(g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id)));
+ g_hash_table_remove(priv->devices, GUINT_TO_POINTER(id));
+
+ g_signal_emit_by_name(self, "device-removed", device);
+ g_object_notify(G_OBJECT(self), "devices");
+ g_object_unref(device);
+ }
+}
+
+static void astal_wp_wp_objm_installed(AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ astal_wp_endpoint_init_as_default(self->default_speaker, priv->mixer, priv->defaults,
+ ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER, self);
+ astal_wp_endpoint_init_as_default(self->default_microphone, priv->mixer, priv->defaults,
+ ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE, self);
+}
+
+static void astal_wp_wp_plugin_activated(WpObject *obj, GAsyncResult *result, AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ GError *error = NULL;
+ wp_object_activate_finish(obj, result, &error);
+ if (error) {
+ g_critical("Failed to activate component: %s\n", error->message);
+ return;
+ }
+
+ if (--priv->pending_plugins == 0) {
+ priv->defaults = wp_plugin_find(priv->core, "default-nodes-api");
+ priv->mixer = wp_plugin_find(priv->core, "mixer-api");
+ g_object_set(priv->mixer, "scale", self->scale, NULL);
+
+ g_signal_connect_swapped(priv->obj_manager, "object-added",
+ G_CALLBACK(astal_wp_wp_object_added), self);
+ g_signal_connect_swapped(priv->obj_manager, "object-removed",
+ G_CALLBACK(astal_wp_wp_object_removed), self);
+
+ wp_core_install_object_manager(priv->core, priv->obj_manager);
+ }
+}
+
+static void astal_wp_wp_plugin_loaded(WpObject *obj, GAsyncResult *result, AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ GError *error = NULL;
+ wp_core_load_component_finish(priv->core, result, &error);
+ if (error) {
+ g_critical("Failed to load component: %s\n", error->message);
+ return;
+ }
+
+ wp_object_activate(obj, WP_PLUGIN_FEATURE_ENABLED, NULL,
+ (GAsyncReadyCallback)astal_wp_wp_plugin_activated, self);
+}
+
+/**
+ * astal_wp_wp_get_default
+ *
+ * Returns: (nullable) (transfer none): gets the default wireplumber object.
+ */
+AstalWpWp *astal_wp_wp_get_default() {
+ static AstalWpWp *self = NULL;
+
+ if (self == NULL) self = g_object_new(ASTAL_WP_TYPE_WP, NULL);
+
+ return self;
+}
+
+/**
+ * astal_wp_get_default_wp
+ *
+ * Returns: (nullable) (transfer none): gets the default wireplumber object.
+ */
+AstalWpWp *astal_wp_get_default_wp() { return astal_wp_wp_get_default(); }
+
+static void astal_wp_wp_dispose(GObject *object) {
+ AstalWpWp *self = ASTAL_WP_WP(object);
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ g_clear_object(&self->video);
+ g_clear_object(&self->audio);
+
+ wp_core_disconnect(priv->core);
+ g_clear_object(&self->default_speaker);
+ g_clear_object(&self->default_microphone);
+ g_clear_object(&priv->mixer);
+ g_clear_object(&priv->defaults);
+ g_clear_object(&priv->obj_manager);
+ g_clear_object(&priv->core);
+
+ if (priv->endpoints != NULL) {
+ g_hash_table_destroy(priv->endpoints);
+ priv->endpoints = NULL;
+ }
+}
+
+static void astal_wp_wp_finalize(GObject *object) {
+ AstalWpWp *self = ASTAL_WP_WP(object);
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+}
+
+static void astal_wp_wp_init(AstalWpWp *self) {
+ AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self);
+
+ priv->endpoints = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref);
+ priv->devices = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref);
+
+ wp_init(7);
+ priv->core = wp_core_new(NULL, NULL, NULL);
+
+ if (!wp_core_connect(priv->core)) {
+ g_critical("could not connect to PipeWire\n");
+ return;
+ }
+
+ priv->obj_manager = wp_object_manager_new();
+ wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_NODE,
+ WP_OBJECT_FEATURES_ALL);
+ wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_GLOBAL_PROXY,
+ WP_OBJECT_FEATURES_ALL);
+
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Audio/Sink", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Audio/Source", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Stream/Output/Audio", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Stream/Input/Audio", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE,
+ WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s",
+ "Audio/Device", NULL);
+
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Video/Sink", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Video/Source", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Stream/Output/Video", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ "media.class", "=s", "Stream/Input/Video", NULL);
+ wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE,
+ WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s",
+ "Video/Device", NULL);
+ // wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_CLIENT, NULL);
+
+ g_signal_connect_swapped(priv->obj_manager, "installed", (GCallback)astal_wp_wp_objm_installed,
+ self);
+
+ self->default_speaker = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL);
+ self->default_microphone = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL);
+
+ self->audio = astal_wp_audio_new(self);
+ self->video = astal_wp_video_new(self);
+
+ priv->pending_plugins = 2;
+ wp_core_load_component(priv->core, "libwireplumber-module-default-nodes-api", "module", NULL,
+ "default-nodes-api", NULL,
+ (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self);
+ wp_core_load_component(priv->core, "libwireplumber-module-mixer-api", "module", NULL,
+ "mixer-api", NULL, (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self);
+}
+
+static void astal_wp_wp_class_init(AstalWpWpClass *class) {
+ GObjectClass *object_class = G_OBJECT_CLASS(class);
+ object_class->finalize = astal_wp_wp_finalize;
+ object_class->dispose = astal_wp_wp_dispose;
+ object_class->get_property = astal_wp_wp_get_property;
+ object_class->set_property = astal_wp_wp_set_property;
+
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_AUDIO] =
+ g_param_spec_object("audio", "audio", "audio", ASTAL_WP_TYPE_AUDIO, G_PARAM_READABLE);
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_VIDEO] =
+ g_param_spec_object("video", "video", "video", ASTAL_WP_TYPE_VIDEO, G_PARAM_READABLE);
+ /**
+ * AstalWpWp:scale: (type AstalWpScale)
+ *
+ * The scale used for the volume
+ */
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_SCALE] =
+ g_param_spec_enum("scale", "scale", "scale", ASTAL_WP_TYPE_SCALE, ASTAL_WP_SCALE_CUBIC,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * AstalWpWp:endpoints: (type GList(AstalWpEndpoint)) (transfer container)
+ *
+ * A list of AstalWpEndpoint objects
+ */
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_ENDPOINTS] =
+ g_param_spec_pointer("endpoints", "endpoints", "endpoints", G_PARAM_READABLE);
+ /**
+ * AstalWpWp:devices: (type GList(AstalWpDevice)) (transfer container)
+ *
+ * A list of AstalWpDevice objects
+ */
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEVICES] =
+ g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE);
+ /**
+ * AstalWpWp:default-speaker:
+ *
+ * The AstalWndpoint object representing the default speaker
+ */
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_SPEAKER] =
+ g_param_spec_object("default-speaker", "default-speaker", "default-speaker",
+ ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE);
+ /**
+ * AstalWpWp:default-microphone:
+ *
+ * The AstalWndpoint object representing the default speaker
+ */
+ astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE] =
+ g_param_spec_object("default-microphone", "default-microphone", "default-microphone",
+ ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE);
+
+ g_object_class_install_properties(object_class, ASTAL_WP_WP_N_PROPERTIES,
+ astal_wp_wp_properties);
+
+ astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED] =
+ g_signal_new("endpoint-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT);
+ astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED] =
+ g_signal_new("endpoint-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL,
+ NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT);
+ astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_DEVICE_ADDED] =
+ g_signal_new("device-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE);
+ astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED] =
+ g_signal_new("device-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE);
+}