mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-02-04 05:00:26 +01:00
m-settings: Module to handle JSON config settings.
- parse settings from .conf file.
- create "sm-settings" metadata and copy settings as key value pairs
to it.
- put in place "persistent" behavior, control it with a special
setting.
- when persistent behavior is enabled.
- create a state file and update settings to it.
- monitor changes in the metadata and update the settings to state
file.
This commit is contained in:
parent
7e3b175d93
commit
e9391b195f
3 changed files with 551 additions and 0 deletions
|
|
@ -3,6 +3,17 @@ common_c_args = [
|
|||
'-DG_LOG_USE_STRUCTURED',
|
||||
]
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-settings',
|
||||
[
|
||||
'module-settings.c',
|
||||
],
|
||||
c_args : [common_c_args, '-DG_LOG_DOMAIN="m-settings"'],
|
||||
install : true,
|
||||
install_dir : wireplumber_module_dir,
|
||||
dependencies : [wp_dep, pipewire_dep],
|
||||
)
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-metadata',
|
||||
[
|
||||
|
|
|
|||
330
modules/module-settings.c
Normal file
330
modules/module-settings.c
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2022 Collabora Ltd.
|
||||
* @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <wp/wp.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/utils/defs.h>
|
||||
|
||||
/*
|
||||
* This module parses the "wireplumber.settings" section from the .conf file.
|
||||
*
|
||||
* Creates "sm-settings" metadata and pushes the settings to it. Looks out for
|
||||
* changes done in the metadata via the pw-metadata interface.
|
||||
*
|
||||
* If persistent settings is enabled stores the settings in a state file
|
||||
* and retains the settings from there on subsequent reboots ignoring the
|
||||
* contents of .conf file.
|
||||
*/
|
||||
|
||||
struct _WpSettingsPlugin
|
||||
{
|
||||
WpPlugin parent;
|
||||
|
||||
WpImplMetadata *impl_metadata;
|
||||
|
||||
WpProperties *settings;
|
||||
|
||||
WpState *state;
|
||||
|
||||
GSource *timeout_source;
|
||||
guint save_interval_ms;
|
||||
gboolean use_persistent_storage;
|
||||
};
|
||||
|
||||
G_DECLARE_FINAL_TYPE (WpSettingsPlugin, wp_settings_plugin,
|
||||
WP, SETTINGS_PLUGIN, WpPlugin)
|
||||
G_DEFINE_TYPE (WpSettingsPlugin, wp_settings_plugin, WP_TYPE_PLUGIN)
|
||||
|
||||
#define NAME "sm-settings"
|
||||
#define PERSISTENT_SETTING "persistent.settings"
|
||||
|
||||
static void
|
||||
wp_settings_plugin_init (WpSettingsPlugin * self)
|
||||
{
|
||||
}
|
||||
|
||||
static gboolean
|
||||
timeout_save_state_callback (WpSettingsPlugin *self)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
if (!self->state)
|
||||
self->state = wp_state_new (NAME);
|
||||
|
||||
if (!wp_state_save (self->state, self->settings, &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 (WpSettingsPlugin *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
|
||||
settings_available_in_state_file (WpSettingsPlugin * self)
|
||||
{
|
||||
g_autoptr (WpState) state = wp_state_new (NAME);
|
||||
g_autoptr (WpProperties) settings = wp_state_load (state);
|
||||
guint count = wp_properties_get_count(settings);
|
||||
|
||||
if (count > 0) {
|
||||
|
||||
wp_info_object (self, "%d settings are available in state file", count);
|
||||
self->state = g_steal_pointer (&state);
|
||||
self->use_persistent_storage = true;
|
||||
|
||||
return true;
|
||||
} else
|
||||
wp_info_object (self, "no settings are available in state file");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_changed (WpMetadata *m, guint32 subject,
|
||||
const gchar *setting, const gchar *type, const gchar *new_value, gpointer d)
|
||||
{
|
||||
WpSettingsPlugin *self = WP_SETTINGS_PLUGIN(d);
|
||||
const gchar *old_value = wp_properties_get (self->settings, setting);
|
||||
|
||||
if (!old_value) {
|
||||
wp_info_object (self, "new setting defined \"%s\" = \"%s\"",
|
||||
setting, new_value);
|
||||
} else {
|
||||
wp_info_object (self, "setting \"%s\" new_value changed from \"%s\" ->"
|
||||
" \"%s\"", setting, old_value, new_value);
|
||||
}
|
||||
|
||||
wp_properties_set (self->settings, setting, new_value);
|
||||
|
||||
/* update the state */
|
||||
timer_start (self);
|
||||
}
|
||||
|
||||
struct data {
|
||||
WpTransition *transition;
|
||||
int count;
|
||||
WpProperties *settings;
|
||||
};
|
||||
|
||||
static int
|
||||
do_parse_settings (void *data, const char *location,
|
||||
const char *section, const char *str, size_t len)
|
||||
{
|
||||
struct data *d = data;
|
||||
WpTransition *transition = d->transition;
|
||||
WpSettingsPlugin *self = wp_transition_get_source_object (transition);
|
||||
g_autoptr (WpSpaJson) *json = wp_spa_json_new_from_stringn (str, len);
|
||||
g_autoptr (WpIterator) *iter = wp_spa_json_new_iterator (json);
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
|
||||
if (!wp_spa_json_is_object (json)) {
|
||||
/* "wireplumber.settings" section has to be a JSON object element. */
|
||||
wp_transition_return_error (transition, g_error_new (
|
||||
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
||||
"failed to parse \"wireplumber.settings\" settings cannot be loaded"));
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
while (wp_iterator_next (iter, &item)) {
|
||||
WpSpaJson *j = g_value_get_boxed (&item);
|
||||
g_autofree gchar *name = wp_spa_json_parse_string (j);
|
||||
g_autofree gchar *value = NULL;
|
||||
int len = 0;
|
||||
|
||||
g_value_unset (&item);
|
||||
wp_iterator_next (iter, &item);
|
||||
j = g_value_get_boxed (&item);
|
||||
|
||||
value = wp_spa_json_parse_string (j);
|
||||
len = wp_spa_json_get_size (j);
|
||||
g_value_unset (&item);
|
||||
|
||||
if (name && value) {
|
||||
wp_debug_object (self, "%s(%d) = %s", name, len, value);
|
||||
|
||||
wp_properties_set (d->settings, name, value);
|
||||
|
||||
if (g_str_equal (name, PERSISTENT_SETTING) && spa_atob (value)) {
|
||||
self->use_persistent_storage = true;
|
||||
wp_info_object (self, "Persistent settings enabled");
|
||||
}
|
||||
|
||||
d->count++;
|
||||
}
|
||||
}
|
||||
|
||||
wp_info_object (self, "parsed %d settings & rules from conf file", d->count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
on_metadata_activated (WpMetadata * m, GAsyncResult * res, gpointer user_data)
|
||||
{
|
||||
WpTransition *transition = WP_TRANSITION (user_data);
|
||||
WpSettingsPlugin *self = wp_transition_get_source_object (transition);
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
struct pw_context *pw_ctx = wp_core_get_pw_context (core);
|
||||
g_autoptr (WpProperties) settings = wp_properties_new_empty();
|
||||
struct data data = { .transition = transition,
|
||||
.settings = settings };
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
|
||||
if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) {
|
||||
g_clear_object (&self->impl_metadata);
|
||||
g_prefix_error (&error, "Failed to activate \"sm-settings\": \
|
||||
Metadata object ");
|
||||
wp_transition_return_error (transition, g_steal_pointer (&error));
|
||||
return;
|
||||
}
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
||||
|
||||
|
||||
if (pw_context_conf_section_for_each (pw_ctx, "wireplumber.settings",
|
||||
do_parse_settings, &data) < 0)
|
||||
return;
|
||||
|
||||
if (data.count == 0) {
|
||||
/*
|
||||
* either the "wireplumber.settings" is not found or not defined as a
|
||||
* valid JSON object element.
|
||||
*/
|
||||
wp_transition_return_error (transition, g_error_new (
|
||||
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
||||
"No settings present in the context conf file: settings"
|
||||
"are not loaded"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self->use_persistent_storage) {
|
||||
|
||||
/* use parsed settings from .conf file */
|
||||
self->settings = g_steal_pointer (&settings);
|
||||
wp_info_object(self, "use settings from .conf file");
|
||||
|
||||
if (settings_available_in_state_file (self)) {
|
||||
wp_info_object (self, "persistant storage is disabled clear the"
|
||||
" settings in the state file");
|
||||
|
||||
wp_state_clear (self->state);
|
||||
g_clear_object (&self->state);
|
||||
}
|
||||
|
||||
} else if (self->use_persistent_storage) {
|
||||
|
||||
/* consider using settings from state file */
|
||||
if (settings_available_in_state_file (self)) {
|
||||
self->settings = wp_state_load (self->state);
|
||||
wp_info_object (self, "persistant storage enabled and settings are found"
|
||||
" in state file, use them");
|
||||
}
|
||||
else
|
||||
{
|
||||
wp_info_object (self, "persistant storage enabled but settings are"
|
||||
" not found in state file so load from .conf file");
|
||||
self->settings = g_steal_pointer (&settings);
|
||||
|
||||
/* save state after time out */
|
||||
timer_start (self);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (it = wp_properties_new_iterator (self->settings);
|
||||
wp_iterator_next (it, &item);
|
||||
g_value_unset (&item)) {
|
||||
WpPropertiesItem *pi = g_value_get_boxed (&item);
|
||||
|
||||
const gchar *setting = wp_properties_item_get_key (pi);
|
||||
const gchar *value = wp_properties_item_get_value (pi);
|
||||
|
||||
wp_debug_object (self, "%s(%lu) = %s", setting, strlen(value), value);
|
||||
wp_metadata_set (m, 0, setting, "Spa:String:JSON", value);
|
||||
}
|
||||
wp_info_object(self, "loaded settings(%d) to \"sm-settings\" metadata",
|
||||
wp_properties_get_count (self->settings));
|
||||
|
||||
|
||||
/* monitor changes in metadata. */
|
||||
g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed),
|
||||
self, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_settings_plugin_enable (WpPlugin * plugin, WpTransition * transition)
|
||||
{
|
||||
WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
|
||||
g_return_if_fail (core);
|
||||
|
||||
self->use_persistent_storage = false;
|
||||
|
||||
/* create metadata object */
|
||||
self->impl_metadata = wp_impl_metadata_new_full (core, "sm-settings", NULL);
|
||||
wp_object_activate (WP_OBJECT (self->impl_metadata),
|
||||
WP_OBJECT_FEATURES_ALL,
|
||||
NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated,
|
||||
transition);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_settings_plugin_disable (WpPlugin * plugin)
|
||||
{
|
||||
WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);
|
||||
|
||||
if (self->timeout_source)
|
||||
g_source_destroy (self->timeout_source);
|
||||
g_clear_pointer (&self->timeout_source, g_source_unref);
|
||||
|
||||
g_clear_pointer (&self->settings, wp_properties_unref);
|
||||
g_clear_object (&self->impl_metadata);
|
||||
|
||||
g_clear_object (&self->state);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_settings_plugin_class_init (WpSettingsPluginClass * klass)
|
||||
{
|
||||
WpPluginClass *plugin_class = (WpPluginClass *) klass;
|
||||
|
||||
plugin_class->enable = wp_settings_plugin_enable;
|
||||
plugin_class->disable = wp_settings_plugin_disable;
|
||||
}
|
||||
|
||||
WP_PLUGIN_EXPORT gboolean
|
||||
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
||||
{
|
||||
wp_plugin_register (g_object_new (wp_settings_plugin_get_type (),
|
||||
"name", "settings",
|
||||
"core", core,
|
||||
NULL));
|
||||
return TRUE;
|
||||
}
|
||||
|
|
@ -85,9 +85,219 @@ wireplumber.components = [
|
|||
# The lua scripting engine
|
||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
||||
|
||||
# Parses all the wireplumber settings in the .conf file, loads them into a
|
||||
# "sm-settings" pipewire metadata and updates the settings to a state file,
|
||||
# when persitent behavior is enabled.
|
||||
|
||||
{ name = libwireplumber-module-settings, type = module }
|
||||
|
||||
# The lua configuration file(s)
|
||||
# Other components are loaded from there
|
||||
{ name = main.lua, type = config/lua }
|
||||
{ name = policy.lua, type = config/lua }
|
||||
{ name = bluetooth.lua, type = config/lua }
|
||||
]
|
||||
|
||||
wireplumber.settings = {
|
||||
# These settings and rules will be loaded during bootup.
|
||||
#
|
||||
# wireplumber JSON settings interface.
|
||||
# <name> = <value>
|
||||
#
|
||||
#
|
||||
# wireplumber rule/logic based JSON settings interface.
|
||||
# <name> = [
|
||||
# {
|
||||
# matches = [
|
||||
# {
|
||||
# <pipewire property2> = <~value*>
|
||||
## if a value starts with "~" then it triggers regular experssion evaluation
|
||||
# <pipewire property1> = <value>
|
||||
## If properties are grouped like this in a single object, that means they
|
||||
## must all must match ("AND" behaviour). If they are in separate objects
|
||||
## with in the same match, then any object is a separate match
|
||||
## ("OR" behaviour).
|
||||
# }
|
||||
# { <wireplumber property> : <value> }
|
||||
# { <wireplumber property> <value> }
|
||||
## "=", ":" or just a space, all are interpreted the same.
|
||||
## Multiple properties matched in a single object are ANDed and across the
|
||||
## objects are ORed
|
||||
# ]
|
||||
# actions = {
|
||||
# update-props = {
|
||||
# <pipewire property> = <value>,
|
||||
# <wireplumber setting> = <value>,
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# if "persistent.settings" is true, the settings will be read from conf file
|
||||
# only once and for subsequent reboots they will be read from the state files,
|
||||
# till the time the setting is set to false.
|
||||
#
|
||||
persistent.settings = false
|
||||
access.enable-flatpak-portal = true
|
||||
access = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
pipewire.access = "flatpak"
|
||||
media.category = "Manager"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
default_permissions = "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
pipewire.access = "flatpak"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
default_permissions = "rx",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
pipewire.access = "restricted"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
quirks = {
|
||||
default_permissions = "rx",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
# store preferences to the file system and restore them at startup;
|
||||
# when set to false, default nodes and routes are selected based on
|
||||
# their priorities and any runtime changes do not persist after restart
|
||||
device.use-persistent-storage = true
|
||||
|
||||
# the default volume to apply to ACP device nodes, in the linear scale
|
||||
# default-volume = 0.4
|
||||
|
||||
# Whether to auto-switch to echo cancel sink and source nodes or not
|
||||
device.auto-echo-cancel = true
|
||||
|
||||
# Sets the default echo-cancel-sink node name to automatically switch to
|
||||
device.echo-cancel-sink-name = echo-cancel-sink
|
||||
|
||||
# Sets the default echo-cancel-source node name to automatically switch to
|
||||
device.echo-cancel-source-name = echo-cancel-source
|
||||
|
||||
device = [
|
||||
{
|
||||
matches = [
|
||||
# Matches all devices
|
||||
{ device.name = "*" }
|
||||
]
|
||||
actions = {
|
||||
quirks = {
|
||||
profile_names = [off pro-audio]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
stream_default.restore-props = true
|
||||
stream_default.restore-target = true
|
||||
stream_default = [
|
||||
{
|
||||
matches = [
|
||||
# Matches all devices
|
||||
{ application.name = "pw-play" }
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
state.restore-props = false
|
||||
state.restore-target = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
alsa_monitor.alsa.reserve = true
|
||||
alsa_monitor.alsa.midi = true
|
||||
alsa_monitor.alsa.monitoring = true
|
||||
|
||||
alsa_monitor = [
|
||||
{
|
||||
# An array of matches/actions to evaluate.
|
||||
|
||||
# If you want to disable some devices or nodes, you can apply properties per device as the following example.
|
||||
# The name can be found by running pw-cli ls Device, or pw-cli dump Device
|
||||
# matches = [
|
||||
# {
|
||||
# { device.name = "name_of_some_disabled_card" }
|
||||
# }
|
||||
# ]
|
||||
# actions = {
|
||||
# update-props = {
|
||||
# device.disabled = true,
|
||||
# }
|
||||
# }
|
||||
}
|
||||
{
|
||||
# Rules for matching a device or node. It is an array of
|
||||
# properties that all need to match the regexp. If any of the
|
||||
# matches work, the actions are executed for the object.
|
||||
matches = [
|
||||
{
|
||||
# This matches all cards.
|
||||
{ device.name = "alsa_card.*" }
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
#Use ALSA-Card-Profile devices. They use UCM or the profile
|
||||
#configuration to configure the device and mixer settings.
|
||||
api.alsa.use-acp = true,
|
||||
|
||||
#Use UCM instead of profile when available. Can be
|
||||
#disabled to skip trying to use the UCM profile.
|
||||
#api.alsa.use-ucm = true
|
||||
|
||||
#Don't use the hardware mixer for volume control. It
|
||||
#will only use software volume. The mixer is still used
|
||||
#to mute unused paths based on the selected port.
|
||||
#api.alsa.soft-mixer = false
|
||||
|
||||
#Ignore decibel settings of the driver. Can be used to
|
||||
#work around buggy drivers that report wrong values.
|
||||
#api.alsa.ignore-dB = false
|
||||
|
||||
#The profile set to use for the device. Usually this is
|
||||
#"default.conf" but can be changed with a udev rule or here.
|
||||
#device.profile-set = "profileset-name",
|
||||
|
||||
#The default active profile. Is by default set to "Off".
|
||||
#device.profile = "default profile name",
|
||||
|
||||
#Automatically select the best profile. This is the
|
||||
#highest priority available profile. This is disabled
|
||||
#here and instead implemented in the session manager
|
||||
#where it can save and load previous preferences.
|
||||
api.acp.auto-profile = false
|
||||
|
||||
#Automatically switch to the highest priority available port.
|
||||
#This is disabled here and implemented in the session manager instead.
|
||||
api.acp.auto-port = false
|
||||
|
||||
#Other properties can be set here.
|
||||
#device.nick = "My Device"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue