mirror of
https://codeberg.org/dnkl/yambar.git
synced 2025-04-19 19:25:41 +02:00
modules/pipewire: new module
This commit is contained in:
parent
dcf21f0b06
commit
19a9f099e2
8 changed files with 1075 additions and 2 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
83
doc/yambar-modules-pipewire.5.scd
Normal file
83
doc/yambar-modules-pipewire.5.scd
Normal file
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]]}
|
||||
|
|
969
modules/pipewire.c
Normal file
969
modules/pipewire.c
Normal file
|
@ -0,0 +1,969 @@
|
|||
#include "spa/utils/list.h"
|
||||
#include <json-c/json.h>
|
||||
#include <pipewire/impl-metadata.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <poll.h>
|
||||
#include <spa/debug/pod.h>
|
||||
#include <spa/pod/iter.h>
|
||||
#include <spa/pod/parser.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/utils/result.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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
|
6
plugin.c
6
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)
|
||||
|
|
Loading…
Add table
Reference in a new issue