Files
wireplumber/modules/module-si-standard-link.c
George Kiagiadakis 6e67000d5e si-standard-link: fix crash after returning a link error
If one link fails, the activation transition will return, but then
other links will continue to call the callback and try to access
the now invalid activation transition. With this change, the callback
is bound to the lifetime of the transition and will stop being called
after the transition returns

Fixes #76
2021-10-16 09:51:00 +03:00

680 lines
22 KiB
C

/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/debug/types.h>
#include <spa/param/audio/type-info.h>
#define SI_FACTORY_NAME "si-standard-link"
struct _WpSiStandardLink
{
WpSessionItem parent;
/* configuration */
GWeakRef out_item;
GWeakRef in_item;
const gchar *out_item_port_context;
const gchar *in_item_port_context;
gboolean passive;
gboolean passthrough;
/* activate */
GPtrArray *node_links;
guint n_async_ops_wait;
};
static void si_standard_link_link_init (WpSiLinkInterface * iface);
G_DECLARE_FINAL_TYPE (WpSiStandardLink, si_standard_link, WP, SI_STANDARD_LINK,
WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiStandardLink, si_standard_link,
WP_TYPE_SESSION_ITEM,
G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINK, si_standard_link_link_init))
static void
si_standard_link_init (WpSiStandardLink * self)
{
g_weak_ref_init (&self->out_item, NULL);
g_weak_ref_init (&self->in_item, NULL);
}
static void
si_standard_link_reset (WpSessionItem * item)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
/* deactivate first */
wp_object_deactivate (WP_OBJECT (self),
WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);
/* reset */
g_weak_ref_set (&self->out_item, NULL);
g_weak_ref_set (&self->in_item, NULL);
self->out_item_port_context = NULL;
self->in_item_port_context = NULL;
self->passive = FALSE;
self->passthrough = FALSE;
WP_SESSION_ITEM_CLASS (si_standard_link_parent_class)->reset (item);
}
static WpSessionItem *
get_and_validate_item (WpProperties * props, const gchar *key)
{
WpSessionItem *res = NULL;
const gchar *str = NULL;
str = wp_properties_get (props, key);
if (!str || sscanf(str, "%p", &res) != 1 || !WP_IS_SI_LINKABLE (res) ||
!(wp_object_get_active_features (WP_OBJECT (res)) &
WP_SESSION_ITEM_FEATURE_ACTIVE))
return NULL;
return res;
}
static gboolean
si_standard_link_configure (WpSessionItem * item, WpProperties * p)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
WpSessionItem *out_item, *in_item;
const gchar *str;
/* reset previous config */
si_standard_link_reset (item);
out_item = get_and_validate_item (si_props, "out.item");
if (!out_item)
return FALSE;
wp_properties_setf (si_props, "out.item.id", "%u",
wp_session_item_get_id (out_item));
in_item = get_and_validate_item (si_props, "in.item");
if (!in_item)
return FALSE;
wp_properties_setf (si_props, "in.item.id", "%u",
wp_session_item_get_id (in_item));
self->out_item_port_context = wp_properties_get (si_props,
"out.item.port.context");
self->in_item_port_context = wp_properties_get (si_props,
"in.item.port.context");
str = wp_properties_get (si_props, "passive");
self->passive = str && pw_properties_parse_bool (str);
str = wp_properties_get (si_props, "passthrough");
self->passthrough = str && pw_properties_parse_bool (str);
g_weak_ref_set(&self->out_item, out_item);
g_weak_ref_set(&self->in_item, in_item);
wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
wp_session_item_set_properties (WP_SESSION_ITEM (self),
g_steal_pointer (&si_props));
return TRUE;
}
static gpointer
si_standard_link_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
return NULL;
}
static void
si_standard_link_disable_active (WpSessionItem *si)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (si);
g_autoptr (WpSessionItem) si_out = g_weak_ref_get (&self->out_item);
g_autoptr (WpSessionItem) si_in = g_weak_ref_get (&self->in_item);
WpSiAcquisition *out_acquisition, *in_acquisition;
if (si_out) {
out_acquisition = wp_si_linkable_get_acquisition (
WP_SI_LINKABLE (si_out));
if (out_acquisition)
wp_si_acquisition_release (out_acquisition, WP_SI_LINK (self),
WP_SI_LINKABLE (si_out));
}
if (si_in) {
in_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_in));
if (in_acquisition)
wp_si_acquisition_release (in_acquisition, WP_SI_LINK (self),
WP_SI_LINKABLE (si_in));
}
g_clear_pointer (&self->node_links, g_ptr_array_unref);
self->n_async_ops_wait = 0;
wp_object_update_features (WP_OBJECT (self), 0,
WP_SESSION_ITEM_FEATURE_ACTIVE);
}
static void
on_link_activated (WpObject * proxy, GAsyncResult * res,
WpTransition * transition)
{
WpSiStandardLink *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
if (!wp_object_activate_finish (proxy, res, &error)) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
self->n_async_ops_wait--;
if (self->n_async_ops_wait == 0)
wp_object_update_features (WP_OBJECT (self),
WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}
struct port
{
guint32 node_id;
guint32 port_id;
guint32 channel;
gboolean visited;
};
static inline bool
channel_is_aux(guint32 channel)
{
return channel >= SPA_AUDIO_CHANNEL_START_Aux &&
channel <= SPA_AUDIO_CHANNEL_LAST_Aux;
}
static inline int
score_ports(struct port *out, struct port *in)
{
int score = 0;
if (out->channel == in->channel)
score += 100;
else if ((out->channel == SPA_AUDIO_CHANNEL_SL && in->channel == SPA_AUDIO_CHANNEL_RL) ||
(out->channel == SPA_AUDIO_CHANNEL_RL && in->channel == SPA_AUDIO_CHANNEL_SL) ||
(out->channel == SPA_AUDIO_CHANNEL_SR && in->channel == SPA_AUDIO_CHANNEL_RR) ||
(out->channel == SPA_AUDIO_CHANNEL_RR && in->channel == SPA_AUDIO_CHANNEL_SR))
score += 60;
else if ((out->channel == SPA_AUDIO_CHANNEL_FC && in->channel == SPA_AUDIO_CHANNEL_MONO) ||
(out->channel == SPA_AUDIO_CHANNEL_MONO && in->channel == SPA_AUDIO_CHANNEL_FC))
score += 50;
else if (in->channel == SPA_AUDIO_CHANNEL_UNKNOWN ||
in->channel == SPA_AUDIO_CHANNEL_MONO ||
out->channel == SPA_AUDIO_CHANNEL_UNKNOWN ||
out->channel == SPA_AUDIO_CHANNEL_MONO)
score += 10;
else if (channel_is_aux(in->channel) != channel_is_aux(out->channel))
score += 7;
if (score > 0 && !in->visited)
score += 5;
if (score <= 10)
score = 0;
return score;
}
static gboolean
create_links (WpSiStandardLink * self, WpTransition * transition,
GVariant * out_ports, GVariant * in_ports)
{
g_autoptr (GArray) in_ports_arr = NULL;
g_autoptr (WpCore) core = NULL;
struct port out_port = {0};
struct port *in_port;
GVariantIter *iter = NULL;
guint i;
/* tuple format:
uint32 node_id;
uint32 port_id;
uint32 channel; // enum spa_audio_channel
*/
if (!out_ports || !g_variant_is_of_type (out_ports, G_VARIANT_TYPE("a(uuu)")))
return FALSE;
if (!in_ports || !g_variant_is_of_type (in_ports, G_VARIANT_TYPE("a(uuu)")))
return FALSE;
core = wp_object_get_core (WP_OBJECT (self));
g_return_val_if_fail (core, FALSE);
self->n_async_ops_wait = 0;
self->node_links = g_ptr_array_new_with_free_func (g_object_unref);
i = g_variant_n_children (in_ports);
if (i == 0)
return FALSE;
/* transfer the in ports to an array so that we can
mark them when they are linked */
in_ports_arr = g_array_sized_new (FALSE, TRUE, sizeof (struct port), i + 1);
g_array_set_size (in_ports_arr, i + 1);
g_variant_get (in_ports, "a(uuu)", &iter);
i = 0;
do {
in_port = &g_array_index (in_ports_arr, struct port, i++);
} while (g_variant_iter_loop (iter, "(uuu)", &in_port->node_id,
&in_port->port_id, &in_port->channel));
g_variant_iter_free (iter);
/* now loop over the out ports and figure out where they should be linked */
g_variant_get (out_ports, "a(uuu)", &iter);
while (g_variant_iter_loop (iter, "(uuu)", &out_port.node_id,
&out_port.port_id, &out_port.channel))
{
int best_score = 0;
struct port *best_port = NULL;
WpProperties *props = NULL;
WpLink *link;
for (i = 0; i < in_ports_arr->len - 1; i++) {
in_port = &g_array_index (in_ports_arr, struct port, i);
int score = score_ports (&out_port, in_port);
if (score > best_score) {
best_score = score;
best_port = in_port;
}
}
/* not all output ports have to be linked ... */
if (!best_port || best_port->visited)
continue;
best_port->visited = TRUE;
/* Create the properties */
props = wp_properties_new_empty ();
wp_properties_setf (props, PW_KEY_LINK_OUTPUT_NODE, "%u", out_port.node_id);
wp_properties_setf (props, PW_KEY_LINK_OUTPUT_PORT, "%u", out_port.port_id);
wp_properties_setf (props, PW_KEY_LINK_INPUT_NODE, "%u", best_port->node_id);
wp_properties_setf (props, PW_KEY_LINK_INPUT_PORT, "%u", best_port->port_id);
if (self->passive)
wp_properties_set (props, PW_KEY_LINK_PASSIVE, "true");
wp_debug_object (self, "create pw link: %u:%u (%s) -> %u:%u (%s)",
out_port.node_id, out_port.port_id,
spa_debug_type_find_name (spa_type_audio_channel, out_port.channel),
best_port->node_id, best_port->port_id,
spa_debug_type_find_name (spa_type_audio_channel, best_port->channel));
/* create the link */
link = wp_link_new_from_factory (core, "link-factory", props);
g_ptr_array_add (self->node_links, link);
/* activate to ensure it is created without errors */
self->n_async_ops_wait++;
wp_object_activate_closure (WP_OBJECT (link),
WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL, NULL,
g_cclosure_new_object (
(GCallback) on_link_activated, G_OBJECT (transition)));
}
g_variant_iter_free (iter);
return self->node_links->len > 0;
}
static void
get_ports_and_create_links (WpSiStandardLink *self, WpTransition *transition)
{
g_autoptr (WpSiLinkable) si_out = NULL;
g_autoptr (WpSiLinkable) si_in = NULL;
g_autoptr (GVariant) out_ports = NULL;
g_autoptr (GVariant) in_ports = NULL;
si_out = WP_SI_LINKABLE (g_weak_ref_get (&self->out_item));
si_in = WP_SI_LINKABLE (g_weak_ref_get (&self->in_item));
g_return_if_fail (si_out);
g_return_if_fail (si_in);
out_ports = wp_si_linkable_get_ports (si_out, self->out_item_port_context);
in_ports = wp_si_linkable_get_ports (si_in, self->in_item_port_context);
if (!create_links (self, transition, out_ports, in_ports))
wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVARIANT,
"Failed to create links because of wrong ports"));
}
static void
on_adapters_ready (GObject *obj, GAsyncResult * res, gpointer p)
{
WpTransition *transition = p;
WpSiStandardLink *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (obj), res, &error);
if (error) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
/* create links */
get_ports_and_create_links (self, transition);
}
struct adapter
{
WpSiAdapter *si;
gboolean is_device;
gboolean dont_remix;
gboolean unpositioned;
gboolean no_dsp;
WpSpaPod *fmt;
const gchar *mode;
};
static void
adapter_free (struct adapter *a)
{
g_clear_object (&a->si);
g_clear_pointer (&a->fmt, wp_spa_pod_unref);
g_slice_free (struct adapter, a);
}
static void
configure_adapter (WpSiStandardLink *self, WpTransition *transition,
struct adapter *main, struct adapter *other)
{
/* configure other to have the same format with main, if necessary */
if (!main->no_dsp && !other->dont_remix && !other->unpositioned && !main->unpositioned) {
/* if formats are the same, no need to reconfigure */
if (other->fmt && !g_strcmp0 (main->mode, other->mode)
&& wp_spa_pod_equal (main->fmt, other->fmt))
get_ports_and_create_links (self, transition);
else
wp_si_adapter_set_ports_format (other->si, wp_spa_pod_ref (main->fmt),
"dsp", on_adapters_ready, transition);
} else if (main->no_dsp) {
/* if formats are the same, no need to reconfigure */
if (other->fmt && !g_strcmp0 (other->mode, "convert")
&& wp_spa_pod_equal (main->fmt, other->fmt))
get_ports_and_create_links (self, transition);
else
wp_si_adapter_set_ports_format (other->si, wp_spa_pod_ref (main->fmt),
"convert", on_adapters_ready, transition);
} else {
/* dont_remix or unpositioned case */
if (other->fmt)
get_ports_and_create_links (self, transition);
else
wp_si_adapter_set_ports_format (other->si, NULL,
"dsp", on_adapters_ready, transition);
}
}
static void
on_main_adapter_ready (GObject *obj, GAsyncResult * res, gpointer p)
{
WpTransition *transition = p;
WpSiStandardLink *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
struct adapter *main, *other;
wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (obj), res, &error);
if (error) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
main = g_object_get_data (G_OBJECT (transition), "adapter_main");
other = g_object_get_data (G_OBJECT (transition), "adapter_other");
if (self->passthrough) {
wp_si_adapter_set_ports_format (other->si, NULL, "passthrough",
on_adapters_ready, transition);
} else {
/* get the up-to-date formats */
g_clear_pointer (&main->fmt, wp_spa_pod_unref);
g_clear_pointer (&other->fmt, wp_spa_pod_unref);
main->fmt = wp_si_adapter_get_ports_format (main->si, &main->mode);
other->fmt = wp_si_adapter_get_ports_format (other->si, &other->mode);
/* now configure other based on main */
configure_adapter (self, transition, main, other);
}
}
static void
configure_and_link_adapters (WpSiStandardLink *self, WpTransition *transition)
{
struct adapter *out, *in, *main, *other;
const gchar *str = NULL;
out = g_slice_new0 (struct adapter);
in = g_slice_new0 (struct adapter);
out->si = WP_SI_ADAPTER (g_weak_ref_get (&self->out_item));
in->si = WP_SI_ADAPTER (g_weak_ref_get (&self->in_item));
g_return_if_fail (out->si);
g_return_if_fail (in->si);
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.node.type");
out->is_device = !g_strcmp0 (str, "device");
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.node.type");
in->is_device = !g_strcmp0 (str, "device");
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.factory.name");
out->is_device = (str && !g_strcmp0 (str, "si-audio-endpoint") && !in->is_device)
|| out->is_device;
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.factory.name");
in->is_device = (str && !g_strcmp0 (str, "si-audio-endpoint") && !out->is_device)
|| in->is_device;
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "stream.dont-remix");
out->dont_remix = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "stream.dont-remix");
in->dont_remix = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.node.unpositioned");
out->unpositioned = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.node.unpositioned");
in->unpositioned = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.features.no-dsp");
out->no_dsp = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.features.no-dsp");
in->no_dsp = str && pw_properties_parse_bool (str);
wp_debug_object (self, "out [device:%d, dont_remix %d, unpos %d], "
"in: [device %d, dont_remix %d, unpos %d]",
out->is_device, out->dont_remix, out->unpositioned,
in->is_device, in->dont_remix, in->unpositioned);
/* we always use out->si format, unless in->si is device */
if (!out->is_device && in->is_device) {
main = in;
other = out;
} else {
main = out;
other = in;
}
/* always configure both adapters in passthrough mode
if this is a passthrough link */
if (self->passthrough) {
g_object_set_data_full (G_OBJECT (transition), "adapter_main", main,
(GDestroyNotify) adapter_free);
g_object_set_data_full (G_OBJECT (transition), "adapter_other", other,
(GDestroyNotify) adapter_free);
wp_si_adapter_set_ports_format (main->si, NULL, "passthrough",
on_main_adapter_ready, transition);
return;
}
main->fmt = wp_si_adapter_get_ports_format (main->si, &main->mode);
other->fmt = wp_si_adapter_get_ports_format (other->si, &other->mode);
if (main->fmt)
/* ideally, configure other based on main */
configure_adapter (self, transition, main, other);
else if (other->fmt)
/* if main is not configured but other is, do it the other way around */
configure_adapter (self, transition, other, main);
else {
/* no adapter configured, let's configure main first */
g_object_set_data_full (G_OBJECT (transition), "adapter_main", main,
(GDestroyNotify) adapter_free);
g_object_set_data_full (G_OBJECT (transition), "adapter_other", other,
(GDestroyNotify) adapter_free);
wp_si_adapter_set_ports_format (main->si, NULL,
main->no_dsp ? "passthrough" : "dsp", on_main_adapter_ready, transition);
return;
}
adapter_free (main);
adapter_free (other);
}
static void
si_standard_link_do_link (WpSiStandardLink *self, WpTransition *transition)
{
g_autoptr (WpSessionItem) si_out = g_weak_ref_get (&self->out_item);
g_autoptr (WpSessionItem) si_in = g_weak_ref_get (&self->in_item);
if (WP_IS_SI_ADAPTER (si_out) && WP_IS_SI_ADAPTER (si_in))
configure_and_link_adapters (self, transition);
else if (!WP_IS_SI_ADAPTER (si_out) && !WP_IS_SI_ADAPTER (si_in))
get_ports_and_create_links (self, transition);
else
wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVARIANT,
"Adapters cannot be linked with non-adapters"));
}
static void
on_item_acquired (WpSiAcquisition * acq, GAsyncResult * res,
WpTransition * transition)
{
WpSiStandardLink *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
if (!wp_si_acquisition_acquire_finish (acq, res, &error)) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
self->n_async_ops_wait--;
if (self->n_async_ops_wait == 0)
si_standard_link_do_link (self, transition);
}
static void
si_standard_link_enable_active (WpSessionItem *si, WpTransition *transition)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (si);
g_autoptr (WpSessionItem) si_out = NULL;
g_autoptr (WpSessionItem) si_in = NULL;
WpSiAcquisition *out_acquisition = NULL, *in_acquisition = NULL;
if (!wp_session_item_is_configured (si)) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"si-standard-link: item is not configured"));
return;
}
/* make sure in/out items are valid */
si_out = g_weak_ref_get (&self->out_item);
si_in = g_weak_ref_get (&self->in_item);
if (!si_out || !si_in) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"si-standard-link: in/out items are not valid anymore"));
return;
}
/* acquire */
out_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_out));
in_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_in));
if (out_acquisition && in_acquisition)
self->n_async_ops_wait = 2;
else if (out_acquisition || in_acquisition)
self->n_async_ops_wait = 1;
else {
self->n_async_ops_wait = 0;
si_standard_link_do_link (self, transition);
return;
}
if (out_acquisition) {
wp_si_acquisition_acquire (out_acquisition, WP_SI_LINK (self),
WP_SI_LINKABLE (si_out), (GAsyncReadyCallback) on_item_acquired,
transition);
}
if (in_acquisition) {
wp_si_acquisition_acquire (in_acquisition, WP_SI_LINK (self),
WP_SI_LINKABLE (si_in), (GAsyncReadyCallback) on_item_acquired,
transition);
}
}
static void
si_standard_link_finalize (GObject * object)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (object);
g_weak_ref_clear (&self->out_item);
g_weak_ref_clear (&self->in_item);
G_OBJECT_CLASS (si_standard_link_parent_class)->finalize (object);
}
static void
si_standard_link_class_init (WpSiStandardLinkClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpSessionItemClass *si_class = (WpSessionItemClass *) klass;
object_class->finalize = si_standard_link_finalize;
si_class->reset = si_standard_link_reset;
si_class->configure = si_standard_link_configure;
si_class->get_associated_proxy = si_standard_link_get_associated_proxy;
si_class->disable_active = si_standard_link_disable_active;
si_class->enable_active = si_standard_link_enable_active;
}
static GVariant *
si_standard_link_get_registration_info (WpSiLink * item)
{
GVariantBuilder b;
g_variant_builder_init (&b, G_VARIANT_TYPE ("a{ss}"));
return g_variant_builder_end (&b);
}
static WpSiLinkable *
si_standard_link_get_out_item (WpSiLink * item)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
return WP_SI_LINKABLE (g_weak_ref_get (&self->out_item));
}
static WpSiLinkable *
si_standard_link_get_in_item (WpSiLink * item)
{
WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
return WP_SI_LINKABLE (g_weak_ref_get (&self->in_item));
}
static void
si_standard_link_link_init (WpSiLinkInterface * iface)
{
iface->get_registration_info = si_standard_link_get_registration_info;
iface->get_out_item = si_standard_link_get_out_item;
iface->get_in_item = si_standard_link_get_in_item;
}
WP_PLUGIN_EXPORT gboolean
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
{
wp_si_factory_register (core, wp_si_factory_new_simple (SI_FACTORY_NAME,
si_standard_link_get_type ()));
return TRUE;
}