forked from external/yambar
1006 lines
30 KiB
C
1006 lines
30 KiB
C
#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 0
|
|
#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;
|
|
uint16_t linear_volume; /* classic volume */
|
|
uint16_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 = roundf(base_volume * 100);
|
|
output_informations->cubic_volume = roundf(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);
|
|
if (data->loop == NULL) {
|
|
LOG_ERR("failed to instantiate main loop");
|
|
goto err;
|
|
}
|
|
|
|
/* Context */
|
|
data->context = pw_context_new(pw_main_loop_get_loop(data->loop), NULL, 0);
|
|
if (data->context == NULL) {
|
|
LOG_ERR("failed to instantiate pipewire context");
|
|
goto err;
|
|
}
|
|
|
|
/* Core */
|
|
data->core = pw_context_connect(data->context, NULL, 0);
|
|
if (data->core == NULL) {
|
|
LOG_ERR("failed to connect to pipewire");
|
|
goto err;
|
|
}
|
|
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);
|
|
if (data->registry == NULL) {
|
|
LOG_ERR("failed to get core registry");
|
|
goto err;
|
|
}
|
|
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;
|
|
|
|
err:
|
|
if (data->registry != NULL)
|
|
pw_proxy_destroy((struct pw_proxy *)data->registry);
|
|
if (data->core != NULL)
|
|
pw_core_disconnect(data->core);
|
|
if (data->context != NULL)
|
|
pw_context_destroy(data->context);
|
|
if (data->loop != NULL)
|
|
pw_main_loop_destroy(data->loop);
|
|
free(data);
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
pipewire_deinit(struct data *data)
|
|
{
|
|
if (data == NULL)
|
|
return;
|
|
|
|
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(const struct module *module)
|
|
{
|
|
return "pipewire";
|
|
}
|
|
|
|
static struct exposable *
|
|
content(struct module *module)
|
|
{
|
|
struct private *private = module->private;
|
|
|
|
if (private->data == NULL)
|
|
return dynlist_exposable_new(NULL, 0, 0, 0);
|
|
|
|
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;
|
|
if (private->data == NULL)
|
|
return 1;
|
|
|
|
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
|