From 19a9f099e2efae8b81f91cbe2c1c045277247087 Mon Sep 17 00:00:00 2001 From: Ogromny Date: Tue, 13 Dec 2022 10:10:06 +0100 Subject: [PATCH] modules/pipewire: new module --- CHANGELOG.md | 2 + doc/meson.build | 1 + doc/yambar-modules-pipewire.5.scd | 83 +++ meson.build | 8 +- meson_options.txt | 3 + modules/meson.build | 5 + modules/pipewire.c | 969 ++++++++++++++++++++++++++++++ plugin.c | 6 + 8 files changed, 1075 insertions(+), 2 deletions(-) create mode 100644 doc/yambar-modules-pipewire.5.scd create mode 100644 modules/pipewire.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ff4e0..9ff593b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * new module: pulse ([#223][223]). * alsa: `dB` tag ([#202][202]). * mpd: `file` tag ([#219][219]). +* pipewire: add a new module for pipewire ([#224][224]) * on-click: support `next`/`previous` mouse buttons ([#228][228]). [153]: https://codeberg.org/dnkl/yambar/issues/153 @@ -38,6 +39,7 @@ [202]: https://codeberg.org/dnkl/yambar/issues/202 [219]: https://codeberg.org/dnkl/yambar/pulls/219 [223]: https://codeberg.org/dnkl/yambar/pulls/223 +[224]: https://codeberg.org/dnkl/yambar/pulls/224 [228]: https://codeberg.org/dnkl/yambar/pulls/228 diff --git a/doc/meson.build b/doc/meson.build index a956dbc..0d62cb5 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -11,6 +11,7 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', 'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd', 'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd', 'yambar-modules-pulse.5.scd', + 'yambar-modules-pipewire.5.scd', 'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd', 'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd', 'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd', diff --git a/doc/yambar-modules-pipewire.5.scd b/doc/yambar-modules-pipewire.5.scd new file mode 100644 index 0000000..4e0f587 --- /dev/null +++ b/doc/yambar-modules-pipewire.5.scd @@ -0,0 +1,83 @@ +yambar-modules-pipewire(5) + +# NAME +pipewire - Monitors pipewire for volume, mute/unmute, device change + +# TAGS + +[[ *Name* +:[ *Type* +:[ *Description* +| type +: string +: Either "source" (capture) or "sink" (speaker) +| name +: string +: Current device name +| description +: string +: Current device description +| form_factor +: string +: Current device form factor (headset, speaker, mic, etc) +| bus +: string +: Current device bus (bluetooth, alsa, etc) +| icon +: string +: Current device icon name +| muted +: bool +: True if muted, otherwise false +| linear_volume +: range +: Linear volume in percentage (with 0 as min and 100 as max) +| cubic_volume +: range +: Cubic volume (used by pulseaudio) in percentage (with 0 as min and 100 as max) + + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + + +# EXAMPLES + +``` +bar: + left: + - pipewire: + anchors: + volume: &volume + conditions: + muted: {string: {text: "{linear_volume}%", foreground: ff0000ff}} + ~muted: {string: {text: "{linear_volume}%"}} + content: + list: + items: + - map: + conditions: + type == "sink": + map: + conditions: + icon == "audio-headset-bluetooth": + string: {text: "🎧 "} + default: + - ramp: + tag: linear_volume + items: + - string: {text: "🔈 "} + - string: {text: "🔉 "} + - string: {text: "🔊 "} + type == "source": + - string: {text: "🎙 "} + - map: + <<: *volume +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/meson.build b/meson.build index 52f75bb..d480929 100644 --- a/meson.build +++ b/meson.build @@ -131,8 +131,11 @@ yambar = executable( version, dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] + decorations + particles + modules, - c_args: [plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[], - plugin_pulse_enabled? '-DPLUGIN_ENABLED_PULSE':[]], + c_args: [ + plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[], + plugin_pulse_enabled? '-DPLUGIN_ENABLED_PULSE':[], + plugin_pipewire_enabled? '-DPLUGIN_ENABLED_PIPEWIRE':[], + ], build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles', export_dynamic: true, install: true, @@ -172,6 +175,7 @@ summary( { 'Music Player Daemon (MPD)': plugin_mpd_enabled, 'PulseAudio': plugin_pulse_enabled, + 'Pipewire': plugin_pipewire_enabled }, section: 'Optional modules', bool_yn: true diff --git a/meson_options.txt b/meson_options.txt index 3878096..bcfce60 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -11,3 +11,6 @@ option( option( 'plugin-pulse', type: 'feature', value: 'auto', description: 'PulseAudio support') +option( + 'plugin-pipewire', type: 'feature', value: 'auto', + description: 'Pipewire support') diff --git a/modules/meson.build b/modules/meson.build index ddeb491..ddfd747 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -10,6 +10,8 @@ xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11')) # Optional deps mpd = dependency('libmpdclient', required: get_option('plugin-mpd')) plugin_mpd_enabled = mpd.found() +pipewire = dependency('libpipewire-0.3', required: get_option('plugin-pipewire')) +plugin_pipewire_enabled = pipewire.found() pulse = dependency('libpulse', required: get_option('plugin-pulse')) plugin_pulse_enabled = pulse.found() @@ -34,6 +36,9 @@ mod_data = { if plugin_mpd_enabled mod_data += {'mpd': [[], [mpd]]} endif +if plugin_pipewire_enabled + mod_data += {'pipewire': [[], [pipewire, dynlist, json]]} +endif if plugin_pulse_enabled mod_data += {'pulse': [[], [pulse]]} diff --git a/modules/pipewire.c b/modules/pipewire.c new file mode 100644 index 0000000..e369bd2 --- /dev/null +++ b/modules/pipewire.c @@ -0,0 +1,969 @@ +#include "spa/utils/list.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "pipewire" +#define LOG_ENABLE_DBG 1 +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../module.h" +#include "../particle.h" +#include "../particles/dynlist.h" +#include "../plugin.h" +#include "../yml.h" + +#define ARRAY_LENGTH(x) (sizeof((x)) / sizeof((x)[0])) +/* clang-format off */ +#define X_FREE_SET(t, v) do { free((t)); (t) = (v); } while (0) +/* clang-format on */ +#define X_STRDUP(s) ((s) != NULL ? strdup((s)) : NULL) + +struct output_informations { + /* internal */ + uint32_t device_id; + uint32_t card_profile_device_id; + + /* informations */ + bool muted; + uint8_t linear_volume; /* classic volume */ + uint8_t cubic_volume; /* volume a la pulseaudio */ + char *name; + char *description; + char *form_factor; /* headset, headphone, speaker, ..., can be null */ + char *bus; /* alsa, bluetooth, etc */ + char *icon; +}; +static struct output_informations const output_informations_null; + +struct data; +struct private +{ + struct particle *label; + struct data *data; + + /* pipewire related */ + struct output_informations sink_informations; + struct output_informations source_informations; +}; + +/* This struct is needed because when param event occur, the function + * `node_events_param` will receive the corresponding event about the node + * but there's no simple way of knowing from which node the event come from */ +struct node_data { + struct data *data; + /* otherwise is_source */ + bool is_sink; +}; + +/* struct data */ +struct node; +struct data { + /* yambar module */ + struct module *module; + + char *target_sink; + char *target_source; + + struct node *binded_sink; + struct node *binded_source; + + struct node_data node_data_sink; + struct node_data node_data_source; + + /* proxies */ + void *metadata; + void *node_sink; + void *node_source; + + /* main struct */ + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + + /* listener */ + struct spa_hook registry_listener; + struct spa_hook core_listener; + struct spa_hook metadata_listener; + struct spa_hook node_sink_listener; + struct spa_hook node_source_listener; + + /* list */ + struct spa_list node_list; + struct spa_list device_list; + + int sync; +}; + +/* struct Route */ +struct route { + struct device *device; + + struct spa_list link; + + enum spa_direction direction; /* direction */ + int profile_device_id; /* device */ + char *form_factor; /* info.type */ + char *icon_name; /* info.icon-name */ +}; + +static void +route_free(struct route *route) +{ + free(route->form_factor); + free(route->icon_name); + spa_list_remove(&route->link); + free(route); +} + +/* struct Device */ +struct device { + struct data *data; + + struct spa_list link; + uint32_t id; + struct spa_list routes; + + void *proxy; + struct spa_hook listener; +}; + +static void +device_free(struct device *device, struct data *data) +{ + struct route *route = NULL; + spa_list_consume(route, &device->routes, link) route_free(route); + + spa_hook_remove(&device->listener); + pw_proxy_destroy((struct pw_proxy *)device->proxy); + + spa_list_remove(&device->link); + free(device); +} + +static struct route * +route_find_or_create(struct device *device, uint32_t profile_device_id) +{ + struct route *route = NULL; + spa_list_for_each(route, &device->routes, link) + { + if (route->profile_device_id == profile_device_id) + return route; + } + + /* route not found, let's create it */ + route = calloc(1, sizeof(struct route)); + assert(route != NULL); + route->device = device; + route->profile_device_id = profile_device_id; + spa_list_append(&device->routes, &route->link); + return route; +} + +struct node { + struct spa_list link; + uint32_t id; + char *name; +}; + +/* struct node */ +static struct route * +node_find_route(struct data *data, bool is_sink) +{ + struct private *private = data->module->private; + struct output_informations *output_informations = NULL; + + if (is_sink) { + if (data->node_sink == NULL) + return NULL; + output_informations = &private->sink_informations; + } else { + if (data->node_source == NULL) + return NULL; + output_informations = &private->source_informations; + } + + struct device *device = NULL; + spa_list_for_each(device, &data->device_list, link) + { + if (device->id != output_informations->device_id) + continue; + + struct route *route = NULL; + spa_list_for_each(route, &device->routes, link) + { + if (route->profile_device_id == output_informations->card_profile_device_id) + return route; + } + } + + return NULL; +} + +static void +node_unhook_binded_node(struct data *data, bool is_sink) +{ + struct node **target_node = NULL; + struct spa_hook *target_listener = NULL; + void **target_proxy = NULL; + + if (is_sink) { + target_node = &data->binded_sink; + target_listener = &data->node_sink_listener; + target_proxy = &data->node_sink; + } else { + target_node = &data->binded_source; + target_listener = &data->node_source_listener; + target_proxy = &data->node_source; + } + + if (*target_node == NULL) + return; + + spa_hook_remove(target_listener); + pw_proxy_destroy(*target_proxy); + + *target_node = NULL; + *target_proxy = NULL; +} + +static void +node_free(struct node *node, struct data *data) +{ + if (data->binded_sink == node) + node_unhook_binded_node(data, true); + else if (data->binded_source == node) + node_unhook_binded_node(data, false); + + spa_list_remove(&node->link); + free(node->name); + free(node); +} + +/* Device events */ +static void +device_events_info(void *userdata, const struct pw_device_info *info) +{ + struct device *device = userdata; + + /* We only want the "Route" param, which is in Params */ + if (!(info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS)) + return; + + for (size_t i = 0; i < info->n_params; ++i) { + if (info->params[i].id == SPA_PARAM_Route) { + pw_device_enum_params(device->proxy, 0, info->params[i].id, 0, -1, NULL); + break; + } + } +} + +static void +device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) +{ + /* We should only receive ParamRoute */ + assert(spa_pod_is_object_type(param, SPA_TYPE_OBJECT_ParamRoute)); + + struct route data = {0}; + struct spa_pod_prop const *prop = NULL; + + /* device must be present otherwise I can't do anything with the data */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_device); + if (prop == NULL) + return; + spa_pod_get_int(&prop->value, &data.profile_device_id); + + /* same for direction, required too */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_direction); + if (prop == NULL) + return; + char const *direction = NULL; + spa_pod_get_string(&prop->value, &direction); + if (spa_streq(direction, "Output")) + data.direction = SPA_DIRECTION_OUTPUT; + else + data.direction = SPA_DIRECTION_INPUT; + + /* same for info, it's required */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_info); + if (prop == NULL) + return; + + struct spa_pod *iter = NULL; + char const *header = NULL; + SPA_POD_STRUCT_FOREACH(&prop->value, iter) + { + /* no previous header */ + if (header == NULL) { + /* headers are always string */ + if (spa_pod_is_string(iter)) + spa_pod_get_string(iter, &header); + /* otherwise it's the first iteration (number of elements in the struct) */ + continue; + } + + /* Values needed: + * - (string) device.icon_name [icon_name] + * - (string) port.type [form_factor] */ + if (spa_pod_is_string(iter)) { + if (spa_streq(header, "device.icon_name")) + spa_pod_get_string(iter, (char const **)&data.icon_name); + else if (spa_streq(header, "port.type")) { + spa_pod_get_string(iter, (char const **)&data.form_factor); + } + } + + header = NULL; + } + + struct device *device = userdata; + + struct route *route = route_find_or_create(device, data.profile_device_id); + X_FREE_SET(route->form_factor, X_STRDUP(data.form_factor)); + X_FREE_SET(route->icon_name, X_STRDUP(data.icon_name)); + route->direction = data.direction; + + /* set missing informations if possible */ + struct private *private = device->data->module->private; + struct node *binded_node = NULL; + struct output_informations *output_informations = NULL; + + if (route->direction == SPA_DIRECTION_INPUT) { + binded_node = private->data->binded_source; + output_informations = &private->source_informations; + } else { + binded_node = private->data->binded_sink; + output_informations = &private->sink_informations; + } + + /* Node not binded */ + if (binded_node == NULL) + return; + + /* Node's device is the the same as route's device */ + if (output_informations->device_id != route->device->id) + return; + + /* Route is not the Node's device route */ + if (output_informations->card_profile_device_id != route->profile_device_id) + return; + + /* Update missing informations */ + X_FREE_SET(output_informations->form_factor, X_STRDUP(route->form_factor)); + X_FREE_SET(output_informations->icon, X_STRDUP(route->icon_name)); + + device->data->module->bar->refresh(device->data->module->bar); +} + +static struct pw_device_events const device_events = { + PW_VERSION_DEVICE_EVENTS, + .info = device_events_info, + .param = device_events_param, +}; + +/* Node events */ +static void +node_events_info(void *userdata, struct pw_node_info const *info) +{ + struct node_data *node_data = userdata; + struct data *data = node_data->data; + struct private *private = data->module->private; + + if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) { + /* We only need the Props param, so let's try to find it */ + for (size_t i = 0; i < info->n_params; ++i) { + if (info->params[i].id == SPA_PARAM_Props) { + void *target_node = (node_data->is_sink ? data->node_sink : data->node_source); + /* Found it, will emit a param event, the parem will then be handled + * in node_events_param */ + pw_node_enum_params(target_node, 0, info->params[i].id, 0, -1, NULL); + break; + } + } + } + + if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { + struct output_informations *output_informations + = (node_data->is_sink ? &private->sink_informations : &private->source_informations); + struct spa_dict_item const *item = NULL; + + item = spa_dict_lookup_item(info->props, "node.name"); + if (item != NULL) + X_FREE_SET(output_informations->name, X_STRDUP(item->value)); + + item = spa_dict_lookup_item(info->props, "node.description"); + if (item != NULL) + X_FREE_SET(output_informations->description, X_STRDUP(item->value)); + + item = spa_dict_lookup_item(info->props, "device.id"); + if (item != NULL) { + uint32_t value = 0; + spa_atou32(item->value, &value, 10); + output_informations->device_id = value; + } + + item = spa_dict_lookup_item(info->props, "card.profile.device"); + if (item != NULL) { + uint32_t value = 0; + spa_atou32(item->value, &value, 10); + output_informations->card_profile_device_id = value; + } + + /* Device's informations has an more important priority than node's informations */ + /* icon_name */ + struct route *route = node_find_route(data, node_data->is_sink); + if (route != NULL && route->icon_name != NULL) + output_informations->icon = X_STRDUP(route->icon_name); + else { + item = spa_dict_lookup_item(info->props, "device.icon-name"); + if (item != NULL) + X_FREE_SET(output_informations->icon, X_STRDUP(item->value)); + } + /* form_factor */ + if (route != NULL && route->form_factor != NULL) + output_informations->form_factor = X_STRDUP(route->form_factor); + else { + item = spa_dict_lookup_item(info->props, "device.form-factor"); + if (item != NULL) + X_FREE_SET(output_informations->form_factor, X_STRDUP(item->value)); + } + + item = spa_dict_lookup_item(info->props, "device.bus"); + if (item != NULL) + X_FREE_SET(output_informations->bus, X_STRDUP(item->value)); + + data->module->bar->refresh(data->module->bar); + } +} + +static void +node_events_param(void *userdata, __attribute__((unused)) int seq, __attribute__((unused)) uint32_t id, + __attribute__((unused)) uint32_t index, __attribute__((unused)) uint32_t next, + const struct spa_pod *param) +{ + struct node_data *node_data = userdata; + struct data *data = node_data->data; + struct private *private = data->module->private; + + struct output_informations *output_informations + = (node_data->is_sink ? &private->sink_informations : &private->source_informations); + struct spa_pod_prop const *prop = NULL; + + prop = spa_pod_find_prop(param, NULL, SPA_PROP_mute); + if (prop != NULL) { + bool value = false; + spa_pod_get_bool(&prop->value, &value); + output_informations->muted = value; + } + + prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelVolumes); + if (prop != NULL) { + uint32_t n_values = 0; + float *values = spa_pod_get_array(&prop->value, &n_values); + float total = 0.0f; + + /* Array can be empty some times, assume that values have not changed */ + if (n_values != 0) { + for (uint32_t i = 0; i < n_values; ++i) + total += values[i]; + + float base_volume = total / n_values; + output_informations->linear_volume = ceilf(base_volume * 100); + output_informations->cubic_volume = ceilf(cbrtf(base_volume) * 100); + } + } + + data->module->bar->refresh(data->module->bar); +} + +static struct pw_node_events const node_events = { + PW_VERSION_NODE_EVENTS, + .info = node_events_info, + .param = node_events_param, +}; + +/* Metadata events */ +static int +metadata_property(void *userdata, __attribute__((unused)) uint32_t id, char const *key, + __attribute__((unused)) char const *type, char const *value) +{ + struct data *data = userdata; + bool is_sink = false; // true for source mode + char **target_name = NULL; + + /* We only want default.audio.sink or default.audio.source */ + if (spa_streq(key, "default.audio.sink")) { + is_sink = true; + target_name = &data->target_sink; + } else if (spa_streq(key, "default.audio.source")) { + is_sink = false; /* just to be explicit */ + target_name = &data->target_source; + } else + return 0; + + /* Value is NULL when the profile is set to `off`. */ + if (value == NULL) { + node_unhook_binded_node(data, is_sink); + free(*target_name); + *target_name = NULL; + data->module->bar->refresh(data->module->bar); + return 0; + } + + struct json_object *json = json_tokener_parse(value); + struct json_object_iterator json_it = json_object_iter_begin(json); + struct json_object_iterator json_it_end = json_object_iter_end(json); + + while (!json_object_iter_equal(&json_it, &json_it_end)) { + char const *key = json_object_iter_peek_name(&json_it); + if (!spa_streq(key, "name")) { + json_object_iter_next(&json_it); + continue; + } + + /* Found name */ + struct json_object *value = json_object_iter_peek_value(&json_it); + assert(json_object_is_type(value, json_type_string)); + + char const *name = json_object_get_string(value); + /* `auto_null` is the same as `value == NULL` see lines above. */ + if (spa_streq(name, "auto_null")) { + node_unhook_binded_node(data, is_sink); + free(*target_name); + *target_name = NULL; + data->module->bar->refresh(data->module->bar); + break; + } + + /* target_name is the same */ + if (spa_streq(name, *target_name)) + break; + + /* Unhook the binded_node */ + node_unhook_binded_node(data, is_sink); + + /* Update the target */ + free(*target_name); + *target_name = strdup(name); + + /* Sync the core, core_events_done will then try to bind the good node */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); + break; + } + + json_object_put(json); + + return 0; +} + +static struct pw_metadata_events const metadata_events = { + PW_VERSION_METADATA_EVENTS, + .property = metadata_property, +}; + +/* Registry events */ +static void +registry_event_global(void *userdata, uint32_t id, __attribute__((unused)) uint32_t permissions, char const *type, + __attribute__((unused)) uint32_t version, struct spa_dict const *props) +{ + struct data *data = userdata; + + /* New device */ + if (spa_streq(type, PW_TYPE_INTERFACE_Device)) { + struct device *device = calloc(1, sizeof(struct device)); + assert(device != NULL); + device->data = data; + device->id = id; + spa_list_init(&device->routes); + device->proxy = pw_registry_bind(data->registry, id, type, PW_VERSION_DEVICE, 0); + assert(device->proxy != NULL); + pw_device_add_listener(device->proxy, &device->listener, &device_events, device); + + spa_list_append(&data->device_list, &device->link); + } + /* New node */ + else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { + /* Fill a new node */ + struct node *node = calloc(1, sizeof(struct node)); + assert(node != NULL); + node->id = id; + node->name = strdup(spa_dict_lookup(props, PW_KEY_NODE_NAME)); + + /* Store it */ + spa_list_append(&data->node_list, &node->link); + } + /* New metadata */ + else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) { + /* A metadata has already been bind */ + if (data->metadata != NULL) + return; + + /* Target only metadata which has "default" key */ + char const *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); + if (name == NULL || !spa_streq(name, "default")) + return; + + /* Bind metadata */ + data->metadata = pw_registry_bind(data->registry, id, type, PW_VERSION_METADATA, 0); + assert(data->metadata != NULL); + pw_metadata_add_listener(data->metadata, &data->metadata_listener, &metadata_events, data); + } + + /* `core_events_done` will then try to bind the good node */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); +} + +static void +registry_event_global_remove(void *userdata, uint32_t id) +{ + struct data *data = userdata; + + /* Try to find a node with the same `id` */ + struct node *node = NULL, *node_temp = NULL; + spa_list_for_each_safe(node, node_temp, &data->node_list, link) + { + if (node->id == id) { + node_free(node, data); + return; + } + } + + /* No node with this `id` maybe it's a device */ + struct device *device = NULL, *device_temp = NULL; + spa_list_for_each_safe(device, device_temp, &data->device_list, link) + { + if (device->id == id) { + device_free(device, data); + return; + } + } +} + +static struct pw_registry_events const registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void +try_to_bind_node(struct node_data *node_data, char const *target_name, struct node **target_node, void **target_proxy, + struct spa_hook *target_listener) +{ + /* profile deactived */ + if (target_name == NULL) + return; + + struct data *data = node_data->data; + + struct node *node = NULL; + spa_list_for_each(node, &data->node_list, link) + { + if (!spa_streq(target_name, node->name)) + continue; + + /* Found good node */ + + *target_node = node; + *target_proxy = pw_registry_bind(data->registry, node->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0); + assert(*target_proxy != NULL); + pw_node_add_listener(*target_proxy, target_listener, &node_events, node_data); + break; + } +} + +/* Core events */ +static void +core_events_done(void *userdata, uint32_t id, int seq) +{ + struct data *data = userdata; + + if (id != PW_ID_CORE) + return; + + /* Not our seq */ + if (data->sync != seq) + return; + + /* Sync ended, try to bind the node which has the targeted sink or the targeted source */ + + /* Node sink not binded and target_sink is set */ + if (data->binded_sink == NULL && data->target_sink != NULL) + try_to_bind_node(&data->node_data_sink, data->target_sink, &data->binded_sink, &data->node_sink, + &data->node_sink_listener); + + /* Node source not binded and target_source is set */ + if (data->binded_source == NULL && data->target_source != NULL) + try_to_bind_node(&data->node_data_source, data->target_source, &data->binded_source, &data->node_source, + &data->node_source_listener); +} + +static void +core_events_error(void *userdata, uint32_t id, int seq, int res, char const *message) +{ + pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) { + struct data *data = userdata; + pw_main_loop_quit(data->loop); + } +} + +static struct pw_core_events const core_events = { + PW_VERSION_CORE_EVENTS, + .done = core_events_done, + .error = core_events_error, +}; + +/* init, deinit */ +static struct data * +pipewire_init(struct module *module) +{ + pw_init(NULL, NULL); + + /* Data */ + struct data *data = calloc(1, sizeof(struct data)); + assert(data != NULL); + + spa_list_init(&data->node_list); + spa_list_init(&data->device_list); + + /* Main loop */ + data->loop = pw_main_loop_new(NULL); + assert(data->loop != NULL); + + /* Context */ + data->context = pw_context_new(pw_main_loop_get_loop(data->loop), NULL, 0); + assert(data->context != NULL); + + /* Core */ + data->core = pw_context_connect(data->context, NULL, 0); + assert(data->core); + pw_core_add_listener(data->core, &data->core_listener, &core_events, data); + + /* Registry */ + data->registry = pw_core_get_registry(data->core, PW_VERSION_REGISTRY, 0); + assert(data->registry); + pw_registry_add_listener(data->registry, &data->registry_listener, ®istry_events, data); + + /* Sync */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); + + data->module = module; + + /* node_events_param_data */ + data->node_data_sink.data = data; + data->node_data_sink.is_sink = true; + data->node_data_source.data = data; + data->node_data_source.is_sink = false; + + return data; +} + +static void +pipewire_deinit(struct data *data) +{ + struct node *node = NULL; + spa_list_consume(node, &data->node_list, link) node_free(node, data); + + struct device *device = NULL; + spa_list_consume(device, &data->device_list, link) device_free(device, data); + + if (data->metadata) + pw_proxy_destroy((struct pw_proxy *)data->metadata); + spa_hook_remove(&data->registry_listener); + pw_proxy_destroy((struct pw_proxy *)data->registry); + spa_hook_remove(&data->core_listener); + spa_hook_remove(&data->metadata_listener); + pw_core_disconnect(data->core); + pw_context_destroy(data->context); + pw_main_loop_destroy(data->loop); + free(data->target_sink); + free(data->target_source); + pw_deinit(); +} + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + + pipewire_deinit(private->data); + private->label->destroy(private->label); + + /* sink */ + free(private->sink_informations.name); + free(private->sink_informations.description); + free(private->sink_informations.icon); + free(private->sink_informations.form_factor); + free(private->sink_informations.bus); + /* source */ + free(private->source_informations.name); + free(private->source_informations.description); + free(private->source_informations.icon); + free(private->source_informations.form_factor); + free(private->source_informations.bus); + + free(private); + module_default_destroy(module); +} + +static char const * +description(struct module *module) +{ + return "pipewire"; +} + +static struct exposable * +content(struct module *module) +{ + struct private *private = module->private; + + mtx_lock(&module->lock); + + struct exposable *exposables[2]; + size_t exposables_length = ARRAY_LENGTH(exposables); + + struct output_informations const *output_informations = NULL; + + /* sink */ + output_informations + = (private->data->target_sink == NULL ? &output_informations_null : &private->sink_informations); + + struct tag_set sink_tag_set = (struct tag_set){ + .tags = (struct tag *[]){ + tag_new_string(module, "type", "sink"), + tag_new_string(module, "name", output_informations->name), + tag_new_string(module, "description", output_informations->description), + tag_new_string(module, "icon", output_informations->icon), + tag_new_string(module, "form_factor", output_informations->form_factor), + tag_new_string(module, "bus", output_informations->bus), + tag_new_bool(module, "muted", output_informations->muted), + tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100), + tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100), + }, + .count = 9, + }; + + /* source */ + output_informations + = (private->data->target_source == NULL ? &output_informations_null : &private->source_informations); + + struct tag_set source_tag_set = (struct tag_set){ + .tags = (struct tag *[]){ + tag_new_string(module, "type", "source"), + tag_new_string(module, "name", output_informations->name), + tag_new_string(module, "description", output_informations->description), + tag_new_string(module, "icon", output_informations->icon), + tag_new_string(module, "form_factor", output_informations->form_factor), + tag_new_string(module, "bus", output_informations->bus), + tag_new_bool(module, "muted", output_informations->muted), + tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100), + tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100), + }, + .count = 9, + }; + + exposables[0] = private->label->instantiate(private->label, &sink_tag_set); + exposables[1] = private->label->instantiate(private->label, &source_tag_set); + + tag_set_destroy(&sink_tag_set); + tag_set_destroy(&source_tag_set); + + mtx_unlock(&module->lock); + + return dynlist_exposable_new(exposables, exposables_length, 0, 0); +} + +static int +run(struct module *module) +{ + struct private *private = module->private; + struct pw_loop *pw_loop = pw_main_loop_get_loop(private->data->loop); + struct pollfd pollfds[] = { + /* abort_fd */ + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + /* pipewire */ + (struct pollfd){.fd = pw_loop_get_fd(pw_loop), .events = POLLIN}, + }; + + while (true) { + if (poll(pollfds, ARRAY_LENGTH(pollfds), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("Unable to poll: %s", strerror(errno)); + break; + } + + /* abort_fd */ + if (pollfds[0].revents & POLLIN) + break; + + /* pipewire */ + if (!(pollfds[1].revents & POLLIN)) + /* issue happened */ + break; + + int result = pw_loop_iterate(pw_loop, 0); + if (result < 0) { + LOG_ERRNO("Unable to iterate pipewire loop: %s", spa_strerror(result)); + break; + } + } + + return 0; +} + +static struct module * +pipewire_new(struct particle *label) +{ + struct private *private = calloc(1, sizeof(struct private)); + assert(private != NULL); + private->label = label; + + struct module *module = module_common_new(); + module->private = private; + module->run = &run; + module->destroy = &destroy; + module->content = &content; + module->description = &description; + + private->data = pipewire_init(module); + + return module; +} + +static struct module * +from_conf(struct yml_node const *node, struct conf_inherit inherited) +{ + struct yml_node const *content = yml_get_value(node, "content"); + return pipewire_new(conf_to_particle(content, inherited)); +} + +static bool +verify_conf(keychain_t *keychain, struct yml_node const *node) +{ + static struct attr_info const attrs[] = { + MODULE_COMMON_ATTRS, + }; + return conf_verify_dict(keychain, node, attrs); +} + +struct module_iface const module_pipewire_iface = { + .from_conf = &from_conf, + .verify_conf = &verify_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern struct module_iface const iface __attribute__((weak, alias("module_pipewire_iface"))); +#endif diff --git a/plugin.c b/plugin.c index 542cfaa..a17f1ae 100644 --- a/plugin.c +++ b/plugin.c @@ -47,6 +47,9 @@ EXTERN_MODULE(network); #if defined(PLUGIN_ENABLED_PULSE) EXTERN_MODULE(pulse); #endif +#if defined(PLUGIN_ENABLED_PIPEWIRE) +EXTERN_MODULE(pipewire); +#endif EXTERN_MODULE(removables); EXTERN_MODULE(river); EXTERN_MODULE(sway_xkb); @@ -134,6 +137,9 @@ init(void) REGISTER_CORE_MODULE(network, network); #if defined(PLUGIN_ENABLED_PULSE) REGISTER_CORE_MODULE(pulse, pulse); +#endif +#if defined(PLUGIN_ENABLED_PIPEWIRE) + REGISTER_CORE_MODULE(pipewire, pipewire); #endif REGISTER_CORE_MODULE(removables, removables); #if defined(HAVE_PLUGIN_river)