diff --git a/modules/module-standard-event-source.c b/modules/module-standard-event-source.c index 8ecc1a79..7a06e0c6 100644 --- a/modules/module-standard-event-source.c +++ b/modules/module-standard-event-source.c @@ -132,7 +132,8 @@ get_object_type (gpointer obj, WpProperties **properties) static gint get_default_event_priority (const gchar *event_type) { - if (!g_strcmp0 (event_type, "find-target-si-and-link")) + if (!g_strcmp0 (event_type, "find-target-si-and-link") || + !g_strcmp0 (event_type, "select-profile")) return 500; else if (!g_strcmp0 (event_type, "rescan-session")) return -500; diff --git a/src/scripts/device/apply-profile.lua b/src/scripts/device/apply-profile.lua new file mode 100644 index 00000000..b7418c98 --- /dev/null +++ b/src/scripts/device/apply-profile.lua @@ -0,0 +1,56 @@ +-- WirePlumber +-- +-- Copyright © 2022 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT + +cutils = require ("common-utils") + +AsyncEventHook { + name = "apply-profile@device", + after = { "find-stored-profile@device", "find-best-profile@device" }, + interests = { + EventInterest { + Constraint { "event.type", "=", "select-profile" }, + }, + }, + steps = { + start = { + next = "none", + execute = function (event, transition) + local device = event:get_subject () + local profile = event:get_data ("selected-profile") + local dev_name = device.properties ["device.name"] + + if not profile then + Log.info (device, "No profile found to set on " .. dev_name) + transition:advance () + return + end + + for p in device:iterate_params ("Profile") do + local active_profile = cutils.parseParam (p, "Profile") + if active_profile.index == profile.index then + Log.info (device, "Profile " .. profile.name .. " is already set on " .. dev_name) + transition:advance () + return + end + end + + local param = Pod.Object { + "Spa:Pod:Object:Param:Profile", "Profile", + index = profile.index, + } + Log.info (device, "Setting profile " .. profile.name .. " on " .. dev_name) + device:set_param ("Profile", param) + + -- FIXME: add cancellability + -- sync on the pipewire connection to ensure that the param + -- has been configured on the remote device object + Core.sync (function () + transition:advance () + end) + end + }, + } +}:register() diff --git a/src/scripts/device/find-best-profile.lua b/src/scripts/device/find-best-profile.lua new file mode 100644 index 00000000..29ffeac4 --- /dev/null +++ b/src/scripts/device/find-best-profile.lua @@ -0,0 +1,65 @@ +-- WirePlumber +-- +-- Copyright © 2022 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT +-- +-- Find the best profile for a device based on profile priorities and +-- availability + +cutils = require ("common-utils") + +SimpleEventHook { + name = "find-best-profile@device", + after = "find-stored-profile@device", + interests = { + EventInterest { + Constraint { "event.type", "=", "select-profile" }, + }, + }, + execute = function (event) + local selected_profile = event:get_data ("selected-profile") + local device = event:get_subject () + local dev_name = device.properties["device.name"] + local off_profile = nil + local best_profile = nil + local unk_profile = nil + + -- skip hook if profile is already selected + if selected_profile then + return + end + + for p in device:iterate_params ("EnumProfile") do + profile = cutils.parseParam (p, "EnumProfile") + if profile and profile.name ~= "pro-audio" then + if profile.name == "off" then + off_profile = profile + elseif profile.available == "yes" then + if best_profile == nil or profile.priority > best_profile.priority then + best_profile = profile + end + elseif profile.available ~= "no" then + if unk_profile == nil or profile.priority > unk_profile.priority then + unk_profile = profile + end + end + end + end + + if best_profile ~= nil then + selected_profile = best_profile + elseif unk_profile ~= nil then + selected_profile = unk_profile + elseif off_profile ~= nil then + selected_profile = off_profile + end + + if selected_profile then + Log.info (device, string.format ( + "Found best profile '%s' (%d) for device '%s'", + selected_profile.name, selected_profile.index, dev_name)) + event:set_data ("selected-profile", selected_profile) + end + end +}:register() diff --git a/src/scripts/device/select-profile.lua b/src/scripts/device/select-profile.lua new file mode 100644 index 00000000..11ee63fc --- /dev/null +++ b/src/scripts/device/select-profile.lua @@ -0,0 +1,25 @@ +-- WirePlumber +-- +-- Copyright © 2022 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT + +cutils = require ("common-utils") + +SimpleEventHook { + name = "select-profile@device", + interests = { + EventInterest { + Constraint { "event.type", "=", "device-added" }, + }, + EventInterest { + Constraint { "event.type", "=", "device-params-changed" }, + Constraint { "event.subject.param-id", "=", "EnumProfile" }, + }, + }, + execute = function (event) + local source = event:get_source () + local device = event:get_subject () + source:call ("push-event", "select-profile", device, nil) + end +}:register() diff --git a/src/scripts/device/state-profile.lua b/src/scripts/device/state-profile.lua new file mode 100644 index 00000000..9a5d60f9 --- /dev/null +++ b/src/scripts/device/state-profile.lua @@ -0,0 +1,120 @@ +-- WirePlumber +-- +-- Copyright © 2022 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT +-- +-- This file contains all the logic related to saving device profiles +-- to a state file and restoring them later on. + +cutils = require ("common-utils") + +state = State ("default-profile") +state_table = state:load () + +SimpleEventHook { + name = "find-stored-profile@device", + interests = { + EventInterest { + Constraint { "event.type", "=", "select-profile" }, + }, + }, + execute = function (event) + local selected_profile = event:get_data ("selected-profile") + local device = event:get_subject () + local dev_name = device.properties["device.name"] + + -- skip hook if profile is already selected + if selected_profile then + return + end + + if not dev_name then + Log.critical (device, "invalid device.name") + return + end + + local profile_name = state_table[dev_name] + + if profile_name then + for p in device:iterate_params ("EnumProfile") do + local profile = cutils.parseParam (p, "EnumProfile") + if profile.name == profile_name then + selected_profile = profile + break + end + end + end + + if selected_profile then + Log.info (device, string.format ( + "Found stored profile '%s' (%d) for device '%s'", + selected_profile.name, selected_profile.index, dev_name)) + event:set_data ("selected-profile", selected_profile) + end + end +}:register() + +SimpleEventHook { + name = "store-user-selected-profile@device", + interests = { + EventInterest { + Constraint { "event.type", "=", "device-params-changed" }, + Constraint { "event.subject.param-id", "=", "Profile" }, + }, + }, + execute = function (event) + local device = event:get_subject () + + for p in device:iterate_params ("Profile") do + local profile = cutils.parseParam (p, "Profile") + if profile.save then + -- store only if this was a user-generated action (save == true) + updateStoredProfile (device, profile) + end + end + end +}:register() + +function updateStoredProfile (device, profile) + local dev_name = device.properties["device.name"] + local index = nil + + if not dev_name then + Log.critical (device, "invalid device.name") + return + end + + Log.debug (device, string.format ( + "update stored profile to '%s' (%d) for device '%s'", + profile.name, profile.index, dev_name)) + + -- check if the new profile is the same as the current one + if state_table[dev_name] == profile.name then + Log.debug (device, " ... profile is already stored") + return + end + + -- find the full profile from EnumProfile, making also sure that the + -- user / client application has actually set an existing profile + for p in device:iterate_params ("EnumProfile") do + local enum_profile = cutils.parseParam (p, "EnumProfile") + if enum_profile.name == profile.name then + index = enum_profile.index + end + end + + if not index then + Log.info (device, string.format ( + "profile '%s' (%d) is not valid on device '%s'", + profile.name, profile.index, dev_name)) + return + end + + state_table[dev_name] = profile.name + cutils.storeAfterTimeout (state, state_table) + + Log.info (device, string.format ( + "stored profile '%s' (%d) for device '%s'", + profile.name, index, dev_name)) +end