mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-02 05:08:01 +02:00
default-nodes: port to a set of scripts with hooks
This commit is contained in:
parent
d8e461940f
commit
2325178d5c
8 changed files with 486 additions and 789 deletions
|
|
@ -25,17 +25,6 @@ shared_library(
|
|||
dependencies : [wp_dep, pipewire_dep],
|
||||
)
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-default-nodes',
|
||||
[
|
||||
'module-default-nodes.c',
|
||||
],
|
||||
c_args : [common_c_args, '-DG_LOG_DOMAIN="m-default-nodes"'],
|
||||
install : true,
|
||||
install_dir : wireplumber_module_dir,
|
||||
dependencies : [wp_dep, pipewire_dep],
|
||||
)
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-default-nodes-api',
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,778 +0,0 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2020 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <wp/wp.h>
|
||||
#include <errno.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/keys.h>
|
||||
|
||||
#include "module-default-nodes/common.h"
|
||||
|
||||
#define NAME "default-nodes"
|
||||
#define DEFAULT_SAVE_INTERVAL_MS 1000
|
||||
#define DEFAULT_USE_PERSISTENT_STORAGE TRUE
|
||||
#define DEFAULT_AUTO_ECHO_CANCEL TRUE
|
||||
#define DEFAULT_ECHO_CANCEL_SINK_NAME "echo-cancel-sink"
|
||||
#define DEFAULT_ECHO_CANCEL_SOURCE_NAME "echo-cancel-source"
|
||||
#define N_PREV_CONFIGS 16
|
||||
|
||||
/*
|
||||
* Module comes up with the default audio and video devices. It looks for
|
||||
* changes in user preference and the changes in devices(when new devices like
|
||||
* headsets, BT devices, HDMI etc are plugged in or removed). User preference
|
||||
* can be expressed via pavuctrl, gnome settings or metadata etc. These apps
|
||||
* typically update the default.configured.*(default-configured-nodes) keys.
|
||||
* Additionally the user preferences are remembered across reboots.
|
||||
*/
|
||||
|
||||
/*
|
||||
* settings file: device.conf
|
||||
*/
|
||||
|
||||
typedef struct _WpDefaultNode WpDefaultNode;
|
||||
struct _WpDefaultNode
|
||||
{
|
||||
gchar *value;
|
||||
gchar *config_value;
|
||||
gchar *prev_config_value[N_PREV_CONFIGS];
|
||||
};
|
||||
|
||||
struct _WpDefaultNodes
|
||||
{
|
||||
WpPlugin parent;
|
||||
|
||||
WpState *state;
|
||||
WpDefaultNode defaults[N_DEFAULT_NODES];
|
||||
WpObjectManager *metadata_om;
|
||||
WpObjectManager *rescan_om;
|
||||
GSource *timeout_source;
|
||||
|
||||
/* properties */
|
||||
gint save_interval_ms;
|
||||
gboolean use_persistent_storage;
|
||||
gboolean auto_echo_cancel;
|
||||
gchar *echo_cancel_names [2];
|
||||
WpSettings *settings;
|
||||
guintptr settings_sub_id;
|
||||
};
|
||||
|
||||
G_DECLARE_FINAL_TYPE (WpDefaultNodes, wp_default_nodes,
|
||||
WP, DEFAULT_NODES, WpPlugin)
|
||||
G_DEFINE_TYPE (WpDefaultNodes, wp_default_nodes, WP_TYPE_PLUGIN)
|
||||
|
||||
static void
|
||||
init_settings (WpDefaultNodes *self)
|
||||
{
|
||||
self->save_interval_ms = DEFAULT_SAVE_INTERVAL_MS;
|
||||
self->use_persistent_storage = DEFAULT_USE_PERSISTENT_STORAGE;
|
||||
self->auto_echo_cancel = DEFAULT_AUTO_ECHO_CANCEL;
|
||||
self->echo_cancel_names [WP_DIRECTION_INPUT] =
|
||||
g_strdup (DEFAULT_ECHO_CANCEL_SOURCE_NAME);
|
||||
self->echo_cancel_names [WP_DIRECTION_OUTPUT] =
|
||||
g_strdup (DEFAULT_ECHO_CANCEL_SINK_NAME);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_default_nodes_init (WpDefaultNodes * self)
|
||||
{
|
||||
init_settings (self);
|
||||
}
|
||||
|
||||
static void
|
||||
update_prev_config_values (WpDefaultNode *def)
|
||||
{
|
||||
gint pos = N_PREV_CONFIGS - 1;
|
||||
|
||||
if (!def->config_value)
|
||||
return;
|
||||
|
||||
/* Find if the current configured value is already in the stack */
|
||||
for (gint i = 0; i < N_PREV_CONFIGS; ++i) {
|
||||
if (!g_strcmp0(def->config_value, def->prev_config_value[i])) {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos == 0)
|
||||
return;
|
||||
|
||||
/* Insert on top position */
|
||||
g_clear_pointer (&def->prev_config_value[pos], g_free);
|
||||
|
||||
for (gint i = pos; i > 0; --i)
|
||||
def->prev_config_value[i] = def->prev_config_value[i-1];
|
||||
|
||||
def->prev_config_value[0] = g_strdup(def->config_value);
|
||||
}
|
||||
|
||||
static void
|
||||
load_state (WpDefaultNodes * self)
|
||||
{
|
||||
g_autoptr (WpProperties) props = wp_state_load (self->state);
|
||||
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
||||
const gchar *value = wp_properties_get (props, DEFAULT_CONFIG_KEY[i]);
|
||||
|
||||
self->defaults[i].config_value = g_strdup (value);
|
||||
|
||||
for (gint j = 0; j < N_PREV_CONFIGS; ++j) {
|
||||
g_autofree gchar *key = g_strdup_printf("%s.%d", DEFAULT_CONFIG_KEY[i], j);
|
||||
|
||||
value = wp_properties_get (props, key);
|
||||
self->defaults[i].prev_config_value[j] = g_strdup(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
timeout_save_state_callback (WpDefaultNodes *self)
|
||||
{
|
||||
g_autoptr (WpProperties) props = wp_properties_new_empty ();
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
||||
if (self->defaults[i].config_value)
|
||||
wp_properties_set (props, DEFAULT_CONFIG_KEY[i],
|
||||
self->defaults[i].config_value);
|
||||
|
||||
for (gint j = 0; j < N_PREV_CONFIGS; ++j) {
|
||||
g_autofree gchar *key = g_strdup_printf("%s.%d", DEFAULT_CONFIG_KEY[i], j);
|
||||
|
||||
wp_properties_set (props, key, self->defaults[i].prev_config_value[j]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!wp_state_save (self->state, props, &error))
|
||||
wp_warning_object (self, "%s", error->message);
|
||||
|
||||
g_clear_pointer (&self->timeout_source, g_source_unref);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void
|
||||
timer_start (WpDefaultNodes *self)
|
||||
{
|
||||
if (!self->timeout_source && self->use_persistent_storage) {
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
g_return_if_fail (core);
|
||||
|
||||
/* Add the timeout callback */
|
||||
wp_core_timeout_add_closure (core, &self->timeout_source,
|
||||
self->save_interval_ms, g_cclosure_new_object (
|
||||
G_CALLBACK (timeout_save_state_callback), G_OBJECT (self)));
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
node_has_available_routes (WpDefaultNodes * self, WpNode *node)
|
||||
{
|
||||
const gchar *dev_id_str = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), PW_KEY_DEVICE_ID);
|
||||
const gchar *cpd_str = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), "card.profile.device");
|
||||
gint dev_id = dev_id_str ? atoi (dev_id_str) : -1;
|
||||
gint cpd = cpd_str ? atoi (cpd_str) : -1;
|
||||
g_autoptr (WpDevice) device = NULL;
|
||||
gint found = 0;
|
||||
|
||||
if (dev_id == -1 || cpd == -1)
|
||||
return TRUE;
|
||||
|
||||
/* Get the device */
|
||||
device = wp_object_manager_lookup (self->rescan_om, WP_TYPE_DEVICE,
|
||||
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=i", dev_id, NULL);
|
||||
if (!device)
|
||||
return TRUE;
|
||||
|
||||
/* Check if the current device route supports the node card device profile */
|
||||
{
|
||||
g_autoptr (WpIterator) routes = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device),
|
||||
"Route", NULL);
|
||||
for (; wp_iterator_next (routes, &val); g_value_unset (&val)) {
|
||||
WpSpaPod *route = g_value_get_boxed (&val);
|
||||
gint route_device = -1;
|
||||
guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown;
|
||||
|
||||
if (!wp_spa_pod_get_object (route, NULL,
|
||||
"device", "i", &route_device,
|
||||
"available", "?I", &route_avail,
|
||||
NULL))
|
||||
continue;
|
||||
|
||||
if (route_device != cpd)
|
||||
continue;
|
||||
|
||||
if (route_avail == SPA_PARAM_AVAILABILITY_no)
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if available routes support the node card device profile */
|
||||
{
|
||||
g_autoptr (WpIterator) routes = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device),
|
||||
"EnumRoute", NULL);
|
||||
for (; wp_iterator_next (routes, &val); g_value_unset (&val)) {
|
||||
WpSpaPod *route = g_value_get_boxed (&val);
|
||||
guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown;
|
||||
g_autoptr (WpSpaPod) route_devices = NULL;
|
||||
|
||||
if (!wp_spa_pod_get_object (route, NULL,
|
||||
"available", "?I", &route_avail,
|
||||
"devices", "?P", &route_devices,
|
||||
NULL))
|
||||
continue;
|
||||
|
||||
{
|
||||
g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (route_devices);
|
||||
g_auto (GValue) v = G_VALUE_INIT;
|
||||
for (; wp_iterator_next (it, &v); g_value_unset (&v)) {
|
||||
gint32 *d = (gint32 *)g_value_get_pointer (&v);
|
||||
if (d && *d == cpd) {
|
||||
found++;
|
||||
if (route_avail != SPA_PARAM_AVAILABILITY_no)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The node is part of a profile without routes so we assume it
|
||||
* is available. This can happen for Pro Audio profiles */
|
||||
if (found == 0)
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
is_echo_cancel_node (WpDefaultNodes * self, WpNode *node, WpDirection direction)
|
||||
{
|
||||
const gchar *name = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME);
|
||||
const gchar *virtual_str = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_VIRTUAL);
|
||||
gboolean virtual = virtual_str && pw_properties_parse_bool (virtual_str);
|
||||
|
||||
if (!name || !virtual)
|
||||
return FALSE;
|
||||
|
||||
return g_strcmp0 (name, self->echo_cancel_names[direction]) == 0;
|
||||
}
|
||||
|
||||
static WpNode *
|
||||
find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class,
|
||||
const WpDefaultNode *def, WpDirection direction, gint *priority)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
gint highest_prio = 0;
|
||||
WpNode *res = NULL;
|
||||
|
||||
g_return_val_if_fail (media_class, NULL);
|
||||
|
||||
it = wp_object_manager_new_filtered_iterator (self->rescan_om, WP_TYPE_NODE,
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "=s", media_class,
|
||||
NULL);
|
||||
|
||||
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
|
||||
WpNode *node = g_value_get_object (&val);
|
||||
g_autoptr (WpPort) port = wp_object_manager_lookup (self->rescan_om,
|
||||
WP_TYPE_PORT, WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_ID,
|
||||
"=u", wp_proxy_get_bound_id (WP_PROXY (node)),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_PORT_DIRECTION,
|
||||
"=s", direction == WP_DIRECTION_INPUT ? "in" : "out",
|
||||
NULL);
|
||||
if (port) {
|
||||
const gchar *name = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME);
|
||||
const gchar *prio_str = wp_pipewire_object_get_property (
|
||||
WP_PIPEWIRE_OBJECT (node), PW_KEY_PRIORITY_SESSION);
|
||||
gint prio = prio_str ? atoi (prio_str) : -1;
|
||||
|
||||
if (!node_has_available_routes (self, node))
|
||||
continue;
|
||||
|
||||
if (self->auto_echo_cancel && is_echo_cancel_node (self, node, direction))
|
||||
prio += 10000;
|
||||
|
||||
if (name && def->config_value && g_strcmp0 (name, def->config_value) == 0) {
|
||||
prio += 20000 * (N_PREV_CONFIGS + 1);
|
||||
} else if (name) {
|
||||
for (gint i = 0; i < N_PREV_CONFIGS; ++i) {
|
||||
if (!def->prev_config_value[i])
|
||||
continue;
|
||||
|
||||
/* Match by name */
|
||||
if (g_strcmp0 (name, def->prev_config_value[i]) == 0) {
|
||||
prio += (N_PREV_CONFIGS - i) * 20000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prio > highest_prio || res == NULL) {
|
||||
highest_prio = prio;
|
||||
res = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (priority)
|
||||
*priority = highest_prio;
|
||||
return res;
|
||||
}
|
||||
|
||||
static WpNode *
|
||||
find_best_media_classes_node (WpDefaultNodes * self,
|
||||
const gchar **media_classes, const WpDefaultNode *def, WpDirection direction)
|
||||
{
|
||||
gint highest_prio = -1;
|
||||
WpNode *res = NULL;
|
||||
for (guint i = 0; media_classes[i]; i++) {
|
||||
gint prio = -1;
|
||||
WpNode *node = find_best_media_class_node (self, media_classes[i],
|
||||
def, direction, &prio);
|
||||
if (node && (!res || prio > highest_prio)) {
|
||||
highest_prio = prio;
|
||||
res = node;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static WpNode *
|
||||
find_best_node (WpDefaultNodes * self, gint node_t)
|
||||
{
|
||||
const WpDefaultNode *def = &self->defaults[node_t];
|
||||
|
||||
switch (node_t) {
|
||||
case AUDIO_SINK: {
|
||||
const gchar *media_classes[] = {
|
||||
"Audio/Sink",
|
||||
"Audio/Duplex",
|
||||
NULL};
|
||||
return find_best_media_classes_node (self, media_classes, def,
|
||||
WP_DIRECTION_INPUT);
|
||||
}
|
||||
case AUDIO_SOURCE: {
|
||||
const gchar *media_classes[] = {
|
||||
"Audio/Source",
|
||||
"Audio/Source/Virtual",
|
||||
"Audio/Duplex",
|
||||
"Audio/Sink",
|
||||
NULL};
|
||||
return find_best_media_classes_node (self, media_classes, def,
|
||||
WP_DIRECTION_OUTPUT);
|
||||
}
|
||||
case VIDEO_SOURCE: {
|
||||
const gchar *media_classes[] = {
|
||||
"Video/Source",
|
||||
"Video/Source/Virtual",
|
||||
NULL};
|
||||
return find_best_media_classes_node (self, media_classes, def,
|
||||
WP_DIRECTION_OUTPUT);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
reevaluate_default_node (WpDefaultNodes * self, WpMetadata *m, gint node_t)
|
||||
{
|
||||
WpNode *node = NULL;
|
||||
const gchar *node_name = NULL;
|
||||
|
||||
node = find_best_node (self, node_t);
|
||||
if (node)
|
||||
node_name = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node),
|
||||
PW_KEY_NODE_NAME);
|
||||
|
||||
/* store it in the metadata if it was changed */
|
||||
if (node && node_name &&
|
||||
g_strcmp0 (node_name, self->defaults[node_t].value) != 0)
|
||||
{
|
||||
g_autoptr (WpSpaJson) json = NULL;
|
||||
|
||||
g_free (self->defaults[node_t].value);
|
||||
self->defaults[node_t].value = g_strdup (node_name);
|
||||
|
||||
wp_info_object (self, "set default node for %s: %s",
|
||||
NODE_TYPE_STR[node_t], node_name);
|
||||
|
||||
json = wp_spa_json_new_object ("name", "s", node_name, NULL);
|
||||
wp_metadata_set (m, 0, DEFAULT_KEY[node_t], "Spa:String:JSON",
|
||||
wp_spa_json_get_data (json));
|
||||
} else if (!node && self->defaults[node_t].value) {
|
||||
g_clear_pointer (&self->defaults[node_t].value, g_free);
|
||||
wp_info_object (self, "unset default node for %s", NODE_TYPE_STR[node_t]);
|
||||
wp_metadata_set (m, 0, DEFAULT_KEY[node_t], NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
rescan (WpEvent *event, gpointer d)
|
||||
{
|
||||
WpDefaultNodes *self = WP_DEFAULT_NODES (d);
|
||||
g_autoptr (WpMetadata) metadata = NULL;
|
||||
|
||||
/* Get the metadata */
|
||||
metadata = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA,
|
||||
NULL);
|
||||
if (!metadata)
|
||||
return;
|
||||
|
||||
wp_trace_object (self, "re-evaluating defaults");
|
||||
reevaluate_default_node (self, metadata, AUDIO_SINK);
|
||||
reevaluate_default_node (self, metadata, AUDIO_SOURCE);
|
||||
reevaluate_default_node (self, metadata, VIDEO_SOURCE);
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_changed (WpEvent *event, gpointer d)
|
||||
{
|
||||
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
||||
gint node_t = -1;
|
||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||
WpMetadata *m = WP_METADATA (subject);
|
||||
|
||||
g_autoptr (WpProperties) p = wp_event_get_properties (event);
|
||||
guint32 subject_id = atoi (wp_properties_get (p, "event.subject.id"));
|
||||
const gchar *key = wp_properties_get (p, "event.subject.key");
|
||||
const gchar *type = wp_properties_get (p, "event.subject.spa_type");
|
||||
const gchar *value = wp_properties_get (p, "event.subject.value");
|
||||
|
||||
if (subject_id == 0) {
|
||||
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
||||
if (!g_strcmp0 (key, DEFAULT_CONFIG_KEY[i])) {
|
||||
node_t = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node_t != -1) {
|
||||
g_clear_pointer (&self->defaults[node_t].config_value, g_free);
|
||||
|
||||
if (value && !g_strcmp0 (type, "Spa:String:JSON")) {
|
||||
g_autoptr (WpSpaJson) json = wp_spa_json_new_from_string (value);
|
||||
g_autofree gchar *name = NULL;
|
||||
if (wp_spa_json_object_get (json, "name", "s", &name, NULL))
|
||||
self->defaults[node_t].config_value = g_strdup (name);
|
||||
}
|
||||
|
||||
update_prev_config_values (&self->defaults[node_t]);
|
||||
|
||||
wp_debug_object (m, "changed '%s' -> '%s'", key,
|
||||
self->defaults[node_t].config_value);
|
||||
|
||||
/* Save state after specific interval */
|
||||
timer_start (self);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_added (WpEvent *event, gpointer d)
|
||||
{
|
||||
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
g_return_if_fail (core);
|
||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||
WpMetadata *metadata = WP_METADATA (subject);
|
||||
|
||||
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
||||
if (self->defaults[i].config_value) {
|
||||
g_autoptr (WpSpaJson) json = wp_spa_json_new_object (
|
||||
"name", "s", self->defaults[i].config_value, NULL);
|
||||
wp_metadata_set (metadata, 0, DEFAULT_CONFIG_KEY[i], "Spa:String:JSON",
|
||||
wp_spa_json_get_data (json));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
get_settings (WpDefaultNodes *self, const gchar *setting)
|
||||
{
|
||||
if (!setting || (g_str_equal ("device.save-interval-ms", setting)))
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_settings_get (self->settings,
|
||||
"device.save-interval-ms");
|
||||
if (j && !wp_spa_json_parse_int (j, &self->save_interval_ms))
|
||||
wp_warning ("Failed to parse integer in device.save-interval-ms");
|
||||
}
|
||||
|
||||
if (!setting || (g_str_equal ("device.use-persistent-storage", setting)))
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_settings_get (self->settings,
|
||||
"device.use-persistent-storage");
|
||||
if (j && !wp_spa_json_parse_boolean (j, &self->use_persistent_storage))
|
||||
wp_warning ("Failed to parse boolean in device.use-persistent-storage");
|
||||
}
|
||||
|
||||
if (!setting || (g_str_equal ("device.auto-echo-cancel", setting)))
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_settings_get (self->settings,
|
||||
"device.auto-echo-cancel");
|
||||
if (j && !wp_spa_json_parse_boolean (j, &self->auto_echo_cancel))
|
||||
wp_warning ("Failed to parse boolean in device.auto-echo-cancel");
|
||||
}
|
||||
|
||||
if (!setting || (g_str_equal ("device.echo-cancel-sink-name", setting)))
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_settings_get (self->settings,
|
||||
"device.echo-cancel-sink-name");
|
||||
if (j) {
|
||||
gchar *value = wp_spa_json_parse_string (j);
|
||||
if (!value)
|
||||
wp_warning ("Failed to parse string in device.echo-cancel-sink-name");
|
||||
else {
|
||||
g_free (self->echo_cancel_names [WP_DIRECTION_OUTPUT]);
|
||||
self->echo_cancel_names [WP_DIRECTION_OUTPUT] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!setting || (g_str_equal ("device.echo-cancel-source-name", setting)))
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_settings_get (self->settings,
|
||||
"device.echo-cancel-source-name");
|
||||
if (j) {
|
||||
gchar *value = wp_spa_json_parse_string (j);
|
||||
if (!value)
|
||||
wp_warning ("Failed to parse string in device.echo-cancel-source-name");
|
||||
else {
|
||||
g_free (self->echo_cancel_names [WP_DIRECTION_INPUT]);
|
||||
self->echo_cancel_names [WP_DIRECTION_INPUT] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_settings_changed_callback (WpSettings *obj, const gchar *setting,
|
||||
WpSpaJson *value, gpointer user_data)
|
||||
{
|
||||
WpDefaultNodes *self = WP_DEFAULT_NODES (user_data);
|
||||
g_return_if_fail (self);
|
||||
g_return_if_fail (setting);
|
||||
|
||||
get_settings (self, setting);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_default_nodes_enable (WpPlugin * plugin, WpTransition * transition)
|
||||
{
|
||||
WpDefaultNodes * self = WP_DEFAULT_NODES (plugin);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
|
||||
g_return_if_fail (core);
|
||||
g_autoptr (WpEventDispatcher) dispatcher =
|
||||
wp_event_dispatcher_get_instance (core);
|
||||
g_autoptr (WpEventHook) hook = NULL;
|
||||
g_return_if_fail (dispatcher);
|
||||
|
||||
self->settings = wp_settings_get_instance (core, NULL);
|
||||
g_return_if_fail (self->settings);
|
||||
self->settings_sub_id = wp_settings_subscribe (self->settings, "device*",
|
||||
wp_settings_changed_callback, (gpointer) self);
|
||||
|
||||
get_settings (self, NULL);
|
||||
|
||||
/* default metadata added */
|
||||
hook = wp_simple_event_hook_new ("metadata-added@default-nodes",
|
||||
NULL, NULL,
|
||||
g_cclosure_new ((GCallback) on_metadata_added, self, NULL));
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-added",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
wp_event_dispatcher_register_hook (dispatcher, hook);
|
||||
g_clear_object(&hook);
|
||||
|
||||
/* default metadata changed */
|
||||
hook = wp_simple_event_hook_new ("metadata-changed@default-nodes",
|
||||
NULL, NULL,
|
||||
g_cclosure_new ((GCallback) on_metadata_changed, self, NULL));
|
||||
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s",
|
||||
"default.configured.audio.sink",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s",
|
||||
"default.configured.video.sink",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s",
|
||||
"default.configured.audio.source",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
wp_event_dispatcher_register_hook (dispatcher, hook);
|
||||
g_clear_object (&hook);
|
||||
|
||||
/* register rescan hook as an after event */
|
||||
hook = wp_simple_event_hook_new("rescan@default-nodes",
|
||||
NULL, NULL,
|
||||
g_cclosure_new ((GCallback) rescan, self, NULL));
|
||||
|
||||
/* default.configured.audio.sink changed */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s", "default.configured.audio.sink",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
/* default.configured.video.sink changed */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s", "default.configured.video.sink",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
/* default.configured.audio.source changed */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "metadata-changed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.key", "=s", "default.configured.audio.source",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
|
||||
/* new video device node added */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "node-added",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "#s", "Video/*",
|
||||
NULL);
|
||||
|
||||
/* new audio device node added */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "node-added",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "#s", "Audio/*",
|
||||
NULL);
|
||||
|
||||
/* video device node removed */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "node-removed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "#s", "Video/*",
|
||||
NULL);
|
||||
|
||||
/* audio device node removed */
|
||||
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "node-removed",
|
||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "#s", "Audio/*",
|
||||
NULL);
|
||||
|
||||
wp_event_dispatcher_register_hook (dispatcher, hook);
|
||||
g_clear_object (&hook);
|
||||
|
||||
/* Create the rescan object manager */
|
||||
self->rescan_om = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_DEVICE, NULL);
|
||||
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_NODE, NULL);
|
||||
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_PORT, NULL);
|
||||
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_DEVICE,
|
||||
WP_OBJECT_FEATURES_ALL);
|
||||
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_NODE,
|
||||
WP_OBJECT_FEATURES_ALL);
|
||||
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_PORT,
|
||||
WP_OBJECT_FEATURES_ALL);
|
||||
wp_core_install_object_manager (core, self->rescan_om);
|
||||
|
||||
if (self->use_persistent_storage) {
|
||||
self->state = wp_state_new (NAME);
|
||||
load_state (self);
|
||||
}
|
||||
|
||||
/* Create the metadata object manager */
|
||||
self->metadata_om = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
||||
NULL);
|
||||
wp_object_manager_request_object_features (self->metadata_om,
|
||||
WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
|
||||
wp_core_install_object_manager (core, self->metadata_om);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_default_nodes_disable (WpPlugin * plugin)
|
||||
{
|
||||
WpDefaultNodes * self = WP_DEFAULT_NODES (plugin);
|
||||
|
||||
/* Clear the current timeout callback */
|
||||
if (self->timeout_source)
|
||||
g_source_destroy (self->timeout_source);
|
||||
g_clear_pointer (&self->timeout_source, g_source_unref);
|
||||
|
||||
for (guint i = 0; i < N_DEFAULT_NODES; i++) {
|
||||
g_clear_pointer (&self->defaults[i].value, g_free);
|
||||
g_clear_pointer (&self->defaults[i].config_value, g_free);
|
||||
|
||||
for (guint j = 0; j < N_PREV_CONFIGS; j++)
|
||||
g_clear_pointer (&self->defaults[i].prev_config_value[j], g_free);
|
||||
}
|
||||
|
||||
g_clear_object (&self->metadata_om);
|
||||
g_clear_object (&self->rescan_om);
|
||||
g_clear_object (&self->state);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
wp_default_nodes_finalize (GObject * object)
|
||||
{
|
||||
WpDefaultNodes * self = WP_DEFAULT_NODES (object);
|
||||
|
||||
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_INPUT], g_free);
|
||||
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_OUTPUT], g_free);
|
||||
|
||||
if (self->settings_sub_id)
|
||||
wp_settings_unsubscribe (self->settings, self->settings_sub_id);
|
||||
|
||||
G_OBJECT_CLASS (wp_default_nodes_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_default_nodes_class_init (WpDefaultNodesClass * klass)
|
||||
{
|
||||
GObjectClass *object_class = (GObjectClass *) klass;
|
||||
WpPluginClass *plugin_class = (WpPluginClass *) klass;
|
||||
|
||||
object_class->finalize = wp_default_nodes_finalize;
|
||||
|
||||
plugin_class->enable = wp_default_nodes_enable;
|
||||
plugin_class->disable = wp_default_nodes_disable;
|
||||
}
|
||||
|
||||
WP_PLUGIN_EXPORT gboolean
|
||||
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
||||
{
|
||||
|
||||
wp_plugin_register (g_object_new (wp_default_nodes_get_type (),
|
||||
"name", NAME,
|
||||
"core", core,
|
||||
NULL));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
32
src/scripts/default-nodes/apply-default-node.lua
Normal file
32
src/scripts/default-nodes/apply-default-node.lua
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
SimpleEventHook {
|
||||
name = "apply-default-node@node",
|
||||
after = { "find-best-default-node@node", "find-stored-default-node@node",
|
||||
"find-echo-cancel-default-node@node" },
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-default-node" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local props = event:get_properties ()
|
||||
local def_node_type = props ["default-node.type"]
|
||||
local selected_node = event:get_data ("selected-node")
|
||||
|
||||
local om = source:call ("get-object-manager", "metadata")
|
||||
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||
|
||||
if selected_node then
|
||||
metadata:set (0, "default." .. def_node_type, "Spa:String:JSON",
|
||||
Json.Object { ["name"] = selected_node }:to_string ())
|
||||
else
|
||||
metadata:set (0, "default." .. def_node_type, nil, nil)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
38
src/scripts/default-nodes/find-best-default-node.lua
Normal file
38
src/scripts/default-nodes/find-best-default-node.lua
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
SimpleEventHook {
|
||||
name = "find-best-default-node@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-default-node" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local available_nodes = event:get_data ("available-nodes")
|
||||
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||
local selected_node = event:get_data ("selected-node")
|
||||
|
||||
available_nodes = available_nodes and available_nodes:parse ()
|
||||
if not available_nodes then
|
||||
return
|
||||
end
|
||||
|
||||
for _, node_props in ipairs (available_nodes) do
|
||||
-- Highest priority node wins
|
||||
local priority = node_props ["priority.session"]
|
||||
priority = math.tointeger (priority) or 0
|
||||
|
||||
if priority > selected_prio or selected_node == nil then
|
||||
selected_prio = priority
|
||||
selected_node = node_props ["node.name"]
|
||||
end
|
||||
end
|
||||
|
||||
event:set_data ("selected-node-priority", selected_prio)
|
||||
event:set_data ("selected-node", selected_node)
|
||||
end
|
||||
}:register ()
|
||||
69
src/scripts/default-nodes/find-echo-cancel-default-node.lua
Normal file
69
src/scripts/default-nodes/find-echo-cancel-default-node.lua
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
config = require ("device-config")
|
||||
|
||||
enabled = false
|
||||
|
||||
find_echo_cancel_default_node_hook = SimpleEventHook {
|
||||
name = "find-echo-cancel-default-node@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-default-node" },
|
||||
Constraint { "default-node.type", "#", "audio.*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local props = event:get_properties ()
|
||||
local available_nodes = event:get_data ("available-nodes")
|
||||
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||
local selected_node = event:get_data ("selected-node")
|
||||
|
||||
available_nodes = available_nodes and available_nodes:parse ()
|
||||
if not available_nodes then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the part after "audio." (= 6 characters)
|
||||
local srcsink = props ["default-node.type"]:sub (6)
|
||||
|
||||
for _, node_props in ipairs (available_nodes) do
|
||||
if isEchoCancelNode (node_props) then
|
||||
local priority = node_props ["priority.session"]
|
||||
priority = math.tointeger (priority) or 0
|
||||
priority = priority + 10001 - i
|
||||
|
||||
if priority > selected_prio then
|
||||
selected_prio = priority
|
||||
selected_node = name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if selected_node then
|
||||
event:set_data ("selected-node-priority", selected_prio)
|
||||
event:set_data ("selected-node", selected_node)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function isEchoCancelNode (node_props, srcsink)
|
||||
local virtual = cutils.parseBool (node_props ["node.virtual"])
|
||||
return virtual and
|
||||
node_props ["node.name"] == config ["echo-cancel-" .. srcsink .. "-name"]
|
||||
end
|
||||
|
||||
function toggleAutoEchoCancel (enable)
|
||||
if enable and not enabled then
|
||||
find_echo_cancel_default_node_hook:register ()
|
||||
elseif not enable and enabled then
|
||||
find_echo_cancel_default_node_hook:remove ()
|
||||
end
|
||||
end
|
||||
|
||||
config:subscribe ("auto-echo-cancel", toggleAutoEchoCancel)
|
||||
toggleAutoEchoCancel (config.auto_echo_cancel)
|
||||
138
src/scripts/default-nodes/select-default-nodes.lua
Normal file
138
src/scripts/default-nodes/select-default-nodes.lua
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
SimpleEventHook {
|
||||
name = "select-default-nodes@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "rescan-session" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local si_om = source:call ("get-object-manager", "session-item")
|
||||
local devices_om = source:call ("get-object-manager", "device")
|
||||
|
||||
Log.trace ("re-evaluating default nodes")
|
||||
|
||||
-- Audio Sink
|
||||
pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.sink", "in", {
|
||||
"Audio/Sink", "Audio/Duplex"
|
||||
})
|
||||
|
||||
-- Audio Source
|
||||
pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.source", "out", {
|
||||
"Audio/Source", "Audio/Source/Virtual", "Audio/Duplex", "Audio/Sink"
|
||||
})
|
||||
|
||||
-- Video Source
|
||||
pushSelectDefaultNodeEvent (source, si_om, devices_om, "video.source", "out", {
|
||||
"Video/Source", "Video/Source/Virtual"
|
||||
})
|
||||
end
|
||||
}:register ()
|
||||
|
||||
function pushSelectDefaultNodeEvent (source, si_om, devices_om, def_node_type,
|
||||
port_direction, media_classes)
|
||||
local nodes =
|
||||
collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
|
||||
local event = source:call ("create-event", "select-default-node", nil, {
|
||||
["default-node.type"] = def_node_type,
|
||||
})
|
||||
event:set_data ("available-nodes", Json.Array (nodes))
|
||||
EventDispatcher.push_event (event)
|
||||
end
|
||||
|
||||
-- Return an array table where each element is another table containing all the
|
||||
-- node properties of all the nodes that can be selected for a given media class
|
||||
-- set and direction
|
||||
function collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
|
||||
local collected = {}
|
||||
|
||||
for linkable in si_om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "media.class", "c", table.unpack (media_classes) },
|
||||
} do
|
||||
local linkable_props = linkable.properties
|
||||
local node = linkable:get_associated_proxy ("node")
|
||||
|
||||
-- check that the node has ports in the requested direction
|
||||
if not node:lookup_port {
|
||||
Constraint { "port.direction", "=", port_direction }
|
||||
} then
|
||||
goto next_linkable
|
||||
end
|
||||
|
||||
-- check that the node has available routes,
|
||||
-- if it is associated to a real device
|
||||
if not nodeHasAvailableRoutes (node, devices_om) then
|
||||
goto next_linkable
|
||||
end
|
||||
|
||||
table.insert (collected, Json.Object (node.properties))
|
||||
|
||||
::next_linkable::
|
||||
end
|
||||
|
||||
return collected
|
||||
end
|
||||
|
||||
-- If the node has an associated device, verify that it has an available
|
||||
-- route. Some UCM profiles expose all paths (headphones, HDMI, etc) as nodes,
|
||||
-- even though they may not be connected... See #145
|
||||
function nodeHasAvailableRoutes (node, devices_om)
|
||||
local properties = node.properties
|
||||
local device_id = properties ["device.id"]
|
||||
local cpd = properties ["card.profile.device"]
|
||||
|
||||
if not device_id or not cpd then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Get the device
|
||||
local device = devices_om:lookup {
|
||||
Constraint { "bound-id", "=", device_id, type = "gobject" }
|
||||
}
|
||||
if not device then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if the current device route supports the node card device profile
|
||||
for r in device:iterate_params ("Route") do
|
||||
local route = r:parse ()
|
||||
if route.device == cpd then
|
||||
if route.available == "no" then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if available routes support the node card device profile
|
||||
local found = 0
|
||||
for r in device:iterate_params ("EnumRoute") do
|
||||
local route = r:parse ()
|
||||
if type (route.devices) == "table" then
|
||||
for _, i in ipairs (route.devices) do
|
||||
if i == cpd then
|
||||
found = found + 1
|
||||
if route.available ~= "no" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The node is part of a profile without routes so we assume it
|
||||
-- is available. This can happen for Pro Audio profiles
|
||||
if found == 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
206
src/scripts/default-nodes/state-default-nodes.lua
Normal file
206
src/scripts/default-nodes/state-default-nodes.lua
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
config = require ("device-config")
|
||||
|
||||
-- the state storage
|
||||
state = nil
|
||||
state_table = nil
|
||||
|
||||
find_stored_default_node_hook = SimpleEventHook {
|
||||
name = "find-stored-default-node@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-default-node" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local props = event:get_properties ()
|
||||
local available_nodes = event:get_data ("available-nodes")
|
||||
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||
local selected_node = event:get_data ("selected-node")
|
||||
|
||||
available_nodes = available_nodes and available_nodes:parse ()
|
||||
if not available_nodes then
|
||||
return
|
||||
end
|
||||
|
||||
local stored = collectStored (props ["default-node.type"])
|
||||
|
||||
-- Check if any of the available nodes matches any of the configured
|
||||
for _, node_props in ipairs (available_nodes) do
|
||||
local name = node_props ["node.name"]
|
||||
|
||||
for i, v in ipairs (stored) do
|
||||
if name == v then
|
||||
local priority = node_props ["priority.session"]
|
||||
priority = math.tointeger (priority) or 0
|
||||
priority = priority + 20001 - i
|
||||
|
||||
if priority > selected_prio then
|
||||
selected_prio = priority
|
||||
selected_node = name
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if selected_node then
|
||||
event:set_data ("selected-node-priority", selected_prio)
|
||||
event:set_data ("selected-node", selected_node)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
rescan_trigger_hook = SimpleEventHook {
|
||||
name = "rescan-trigger-default-nodes@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||
"default.configured.audio.source", "default.configured.video.source"
|
||||
},
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
source:call ("schedule-rescan")
|
||||
end
|
||||
}
|
||||
|
||||
store_configured_default_nodes_hook = SimpleEventHook {
|
||||
name = "store-configured-default-nodes@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||
"default.configured.audio.source", "default.configured.video.source"
|
||||
},
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local props = event:get_properties ()
|
||||
-- get the part after "default.configured." (= 19 chars)
|
||||
local def_node_type = props ["event.subject.key"]:sub (19)
|
||||
local new_value = props ["event.subject.value"]
|
||||
local new_stored = {}
|
||||
|
||||
if new_value then
|
||||
new_value = Json.Raw (new_value):parse ()["name"]
|
||||
end
|
||||
|
||||
if new_value then
|
||||
local stored = collectStored (def_node_type)
|
||||
local pos = #stored + 1
|
||||
|
||||
-- find if the curent configured value is already in the stack
|
||||
for i, v in ipairs (stored) do
|
||||
if v == new_value then
|
||||
pos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- insert at the top and shift the remaining to fill the gap
|
||||
new_stored [1] = new_value
|
||||
if pos > 1 then
|
||||
table.move (stored, 1, pos-1, 2, new_stored)
|
||||
end
|
||||
if pos < #stored then
|
||||
table.move (stored, pos+1, #stored, pos+1, new_stored)
|
||||
end
|
||||
end
|
||||
|
||||
updateStored (def_node_type, new_stored)
|
||||
end
|
||||
}
|
||||
|
||||
-- set initial values
|
||||
metadata_added_hook = SimpleEventHook {
|
||||
name = "metadata-added@node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-added" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local types = { "audio.sink", "audio.source", "video.source" }
|
||||
for _, t in ipairs (types) do
|
||||
local v = state_table ["default.configured." .. t]
|
||||
if v then
|
||||
metadata:set (0, "default.configured." .. t, "Spa:String:JSON",
|
||||
Json.Object { ["name"] = v }:to_string ())
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
-- Collect all the previously configured node names from the state file
|
||||
function collectStored (def_node_type)
|
||||
local stored = {}
|
||||
local key_base = "default.configured." .. def_node_type
|
||||
local key = key_base
|
||||
|
||||
local index = 0
|
||||
repeat
|
||||
local v = state_table [key]
|
||||
table.insert (stored, v)
|
||||
key = key_base .. "." .. tostring (index)
|
||||
index = index + 1
|
||||
until v == nil
|
||||
|
||||
return stored
|
||||
end
|
||||
|
||||
-- Store the given node names in the state file
|
||||
function updateStored (def_node_type, stored)
|
||||
local key_base = "default.configured." .. def_node_type
|
||||
local key = key_base
|
||||
|
||||
local index = 0
|
||||
for _, v in ipairs (stored) do
|
||||
state_table [key] = v
|
||||
key = key_base .. "." .. tostring (index)
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
-- erase the rest, if any
|
||||
repeat
|
||||
local v = state_table [key]
|
||||
state_table [key] = nil
|
||||
key = key_base .. "." .. tostring (index)
|
||||
index = index + 1
|
||||
until v == nil
|
||||
|
||||
cutils.storeAfterTimeout (state, state_table)
|
||||
end
|
||||
|
||||
function handlePersistentSetting (enable)
|
||||
if enable and not state then
|
||||
state = State ("default-nodes")
|
||||
state_table = state:load ()
|
||||
find_stored_default_node_hook:register ()
|
||||
rescan_trigger_hook:register ()
|
||||
store_configured_default_nodes_hook:register ()
|
||||
metadata_added_hook:register ()
|
||||
elseif not enable and state then
|
||||
state = nil
|
||||
state_table = nil
|
||||
find_stored_default_node_hook:remove ()
|
||||
rescan_trigger_hook:remove ()
|
||||
store_configured_default_nodes_hook:remove ()
|
||||
metadata_added_hook:remove ()
|
||||
end
|
||||
end
|
||||
|
||||
config:subscribe ("use-persistent-storage", handlePersistentSetting)
|
||||
handlePersistentSetting (config.use_persistent_storage)
|
||||
|
|
@ -12,6 +12,9 @@ local defaults = {
|
|||
["use-persistent-storage"] = true,
|
||||
["default-volume"] = 0.4 ^ 3,
|
||||
["default-input-volume"] = 1.0,
|
||||
["auto-echo-cancel"] = true,
|
||||
["echo-cancel-sink-name"] = "echo-cancel-sink",
|
||||
["echo-cancel-source-name"] = "echo-cancel-source",
|
||||
}
|
||||
|
||||
return settings_manager.new ("device.", defaults)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue