Update
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- apply the selected profile to the device
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
AsyncEventHook {
|
||||
name = "device/apply-profile",
|
||||
after = { "device/find-stored-profile", "device/find-preferred-profile", "device/find-best-profile" },
|
||||
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"] or ""
|
||||
|
||||
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 == tonumber(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 = tonumber(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()
|
||||
107
pipewire/.config/wireplumber/scripts/device/apply-routes.lua
Normal file
107
pipewire/.config/wireplumber/scripts/device/apply-routes.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021-2022 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- Based on default-routes.c from pipewire-media-session
|
||||
-- Copyright © 2020 Wim Taymans
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Set the Route param as part of the "select-routes" event run
|
||||
|
||||
devinfo = require ("device-info-cache")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
AsyncEventHook {
|
||||
name = "device/apply-routes",
|
||||
after = { "device/find-stored-routes",
|
||||
"device/find-best-routes",
|
||||
"device/apply-route-props" },
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-routes" },
|
||||
},
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local device = event:get_subject ()
|
||||
local selected_routes = event:get_data ("selected-routes")
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
||||
if not selected_routes then
|
||||
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
for device_id, route in pairs (selected_routes) do
|
||||
-- JSON to lua table
|
||||
route = Json.Raw (route):parse ()
|
||||
|
||||
-- steal the props
|
||||
local props = route.props or {}
|
||||
|
||||
-- replace with the full route info
|
||||
local route_info = devinfo.find_route_info (dev_info, route)
|
||||
if not route_info then
|
||||
goto skip_route
|
||||
end
|
||||
|
||||
-- ensure default values
|
||||
local is_input = (route_info.direction == "Input")
|
||||
props.mute = props.mute or false
|
||||
props.channelVolumes = props.channelVolumes or
|
||||
{ is_input and Settings.get_float ("device.routes.default-source-volume")
|
||||
or Settings.get_float ("device.routes.default-sink-volume") }
|
||||
|
||||
-- prefix the props with correct IDs to create a Pod.Object
|
||||
table.insert (props, 1, "Spa:Pod:Object:Param:Props")
|
||||
table.insert (props, 2, "Route")
|
||||
|
||||
-- convert arrays to Spa Pod
|
||||
if props.channelVolumes then
|
||||
table.insert (props.channelVolumes, 1, "Spa:Float")
|
||||
props.channelVolumes = Pod.Array (props.channelVolumes)
|
||||
end
|
||||
if props.channelMap then
|
||||
table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
|
||||
props.channelMap = Pod.Array (props.channelMap)
|
||||
end
|
||||
if props.iec958Codecs then
|
||||
table.insert (props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
|
||||
props.iec958Codecs = Pod.Array (props.iec958Codecs)
|
||||
end
|
||||
|
||||
-- construct Route param
|
||||
local param = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Route", "Route",
|
||||
index = route_info.index,
|
||||
device = device_id,
|
||||
props = Pod.Object (props),
|
||||
save = route_info.save,
|
||||
}
|
||||
|
||||
log:debug (param,
|
||||
string.format ("setting route(%s) on for device(%s)(%s)",
|
||||
route_info.name, dev_info.name, tostring (device)))
|
||||
|
||||
device:set_param ("Route", param)
|
||||
|
||||
::skip_route::
|
||||
end
|
||||
|
||||
-- FIXME: add cancellability
|
||||
-- sync on the pipewire connection to ensure that the params
|
||||
-- have been configured on the remote device object
|
||||
Core.sync (function ()
|
||||
transition:advance ()
|
||||
end)
|
||||
end
|
||||
},
|
||||
}
|
||||
}:register()
|
||||
@@ -0,0 +1,472 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Asymptotic Inc.
|
||||
-- @author Sanchayan Maity <sanchayan@asymptotic.io>
|
||||
--
|
||||
-- Based on bt-profile-switch.lua in tests/examples
|
||||
-- Copyright © 2021 George Kiagiadakis
|
||||
--
|
||||
-- Based on bluez-autoswitch in media-session
|
||||
-- Copyright © 2021 Pauli Virtanen
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- This script is charged to automatically change BT profiles on a device. If a
|
||||
-- client is linked to the device's loopback source node, the associated BT
|
||||
-- device profile is automatically switched to HSP/HFP. If there is no clients
|
||||
-- linked to the device's loopback source node, the BT device profile is
|
||||
-- switched back to A2DP profile.
|
||||
--
|
||||
-- We switch to the highest priority profile that has an Input route available.
|
||||
-- The reason for this is that we may have microphone enabled with non-HFP
|
||||
-- codecs eg. Faststream.
|
||||
-- When a stream goes away if the list with which we track the streams above
|
||||
-- is empty, then we revert back to the old profile.
|
||||
|
||||
-- settings file: bluetooth.conf
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
device_loopback_sources = {}
|
||||
|
||||
local profile_restore_timeout_msec = 2000
|
||||
|
||||
local INVALID = -1
|
||||
local timeout_source = {}
|
||||
local restore_timeout_source = {}
|
||||
|
||||
local last_profiles = {}
|
||||
|
||||
local active_streams = {}
|
||||
local previous_streams = {}
|
||||
|
||||
function handlePersistentSetting (enable)
|
||||
if enable and state == nil then
|
||||
-- the state storage
|
||||
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
|
||||
and State ("bluetooth-autoswitch") or nil
|
||||
headset_profiles = state and state:load () or {}
|
||||
else
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
end
|
||||
end
|
||||
|
||||
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
||||
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||
end)
|
||||
|
||||
devices_om = ObjectManager {
|
||||
Interest {
|
||||
type = "device",
|
||||
Constraint { "device.api", "=", "bluez5" },
|
||||
}
|
||||
}
|
||||
|
||||
streams_om = ObjectManager {
|
||||
Interest {
|
||||
type = "node",
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "-", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||
}
|
||||
}
|
||||
|
||||
filter_nodes_om = ObjectManager {
|
||||
Interest {
|
||||
type = "node",
|
||||
Constraint { "node.link-group", "+", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
}
|
||||
}
|
||||
|
||||
loopback_nodes_om = ObjectManager {
|
||||
Interest {
|
||||
type = "node",
|
||||
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
||||
Constraint { "node.link-group", "+", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" },
|
||||
}
|
||||
}
|
||||
|
||||
local function saveHeadsetProfile (device, profile_name)
|
||||
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||
headset_profiles [key] = profile_name
|
||||
state:save_after_timeout (headset_profiles)
|
||||
end
|
||||
|
||||
local function getSavedHeadsetProfile (device)
|
||||
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||
return headset_profiles [key]
|
||||
end
|
||||
|
||||
local function saveLastProfile (device, profile_name)
|
||||
last_profiles [device.properties ["device.name"]] = profile_name
|
||||
end
|
||||
|
||||
local function getSavedLastProfile (device)
|
||||
return last_profiles [device.properties ["device.name"]]
|
||||
end
|
||||
|
||||
local function isSwitchedToHeadsetProfile (device)
|
||||
return getSavedLastProfile (device) ~= nil
|
||||
end
|
||||
|
||||
local function findProfile (device, index, name)
|
||||
for p in device:iterate_params ("EnumProfile") do
|
||||
local profile = cutils.parseParam (p, "EnumProfile")
|
||||
if not profile then
|
||||
goto skip_enum_profile
|
||||
end
|
||||
|
||||
Log.debug ("Profile name: " .. profile.name .. ", priority: "
|
||||
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
|
||||
if (index ~= nil and profile.index == index) or
|
||||
(name ~= nil and profile.name == name) then
|
||||
return profile.priority, profile.index, profile.name
|
||||
end
|
||||
|
||||
::skip_enum_profile::
|
||||
end
|
||||
|
||||
return INVALID, INVALID, nil
|
||||
end
|
||||
|
||||
local function getCurrentProfile (device)
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile then
|
||||
return profile.name
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function highestPrioProfileWithInputRoute (device)
|
||||
local profile_priority = INVALID
|
||||
local profile_index = INVALID
|
||||
local profile_name = nil
|
||||
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
-- Parse pod
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
if route.direction ~= "Input" then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
Log.debug ("Route with index: " .. tostring (route.index) .. ", direction: "
|
||||
.. route.direction .. ", name: " .. route.name .. ", description: "
|
||||
.. route.description .. ", priority: " .. route.priority)
|
||||
if route.profiles then
|
||||
for _, v in pairs (route.profiles) do
|
||||
local priority, index, name = findProfile (device, v)
|
||||
if priority ~= INVALID then
|
||||
if profile_priority < priority then
|
||||
profile_priority = priority
|
||||
profile_index = index
|
||||
profile_name = name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
|
||||
return profile_priority, profile_index, profile_name
|
||||
end
|
||||
|
||||
local function hasProfileInputRoute (device, profile_index)
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if route and route.direction == "Input" and route.profiles then
|
||||
for _, v in pairs (route.profiles) do
|
||||
if v == profile_index then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function switchDeviceToHeadsetProfile (dev_id)
|
||||
-- Find the actual device
|
||||
local device = devices_om:lookup {
|
||||
Constraint { "bound-id", "=", dev_id, type = "gobject" }
|
||||
}
|
||||
if device == nil then
|
||||
Log.info ("Device with id " .. tostring(dev_id).. " not found")
|
||||
return
|
||||
end
|
||||
|
||||
-- clear restore callback, if any
|
||||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
end
|
||||
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
if hasProfileInputRoute (device, index) then
|
||||
Log.info ("Current profile has input route, not switching")
|
||||
return
|
||||
end
|
||||
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
Log.info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||
return
|
||||
end
|
||||
|
||||
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||
|
||||
index = INVALID
|
||||
if saved_headset_profile then
|
||||
priority, index, name = findProfile (device, nil, saved_headset_profile)
|
||||
if index ~= INVALID and not hasProfileInputRoute (device, index) then
|
||||
index = INVALID
|
||||
saveHeadsetProfile (device, nil)
|
||||
end
|
||||
end
|
||||
if index == INVALID then
|
||||
priority, index, name = highestPrioProfileWithInputRoute (device)
|
||||
end
|
||||
|
||||
if index ~= INVALID then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
}
|
||||
|
||||
-- store the current profile (needed when restoring)
|
||||
saveLastProfile (device, cur_profile_name)
|
||||
|
||||
-- switch to headset profile
|
||||
Log.info ("Setting profile of '"
|
||||
.. device.properties ["device.description"]
|
||||
.. "' from: " .. cur_profile_name
|
||||
.. " to: " .. name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
Log.warning ("Got invalid index when switching profile")
|
||||
end
|
||||
end
|
||||
|
||||
local function restoreProfile (dev_id)
|
||||
-- Find the actual device
|
||||
local device = devices_om:lookup {
|
||||
Constraint { "bound-id", "=", dev_id, type = "gobject" }
|
||||
}
|
||||
if device == nil then
|
||||
Log.info ("Device with id " .. tostring(dev_id).. " not found")
|
||||
return
|
||||
end
|
||||
|
||||
if not isSwitchedToHeadsetProfile (device) then
|
||||
Log.info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
|
||||
return
|
||||
end
|
||||
|
||||
local profile_name = getSavedLastProfile (device)
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name
|
||||
|
||||
if cur_profile_name then
|
||||
priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
|
||||
if index ~= INVALID and hasProfileInputRoute (device, index) then
|
||||
Log.info ("Setting saved headset profile to: " .. cur_profile_name)
|
||||
saveHeadsetProfile (device, cur_profile_name)
|
||||
end
|
||||
end
|
||||
|
||||
if profile_name then
|
||||
priority, index, name = findProfile (device, nil, profile_name)
|
||||
|
||||
if index ~= INVALID then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
}
|
||||
|
||||
-- clear last profile as we will restore it now
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- restore previous profile
|
||||
Log.info ("Restoring profile of '"
|
||||
.. device.properties ["device.description"]
|
||||
.. "' from: " .. cur_profile_name
|
||||
.. " to: " .. name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
Log.warning ("Failed to restore profile")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function triggerRestoreProfile (dev_id)
|
||||
-- we never restore the device profiles if there are active streams
|
||||
for _, v in pairs (active_streams) do
|
||||
if v == dev_id then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- clear restore callback, if any
|
||||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
end
|
||||
|
||||
-- create new restore callback
|
||||
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
restoreProfile (dev_id)
|
||||
end)
|
||||
end
|
||||
|
||||
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||
-- source filter
|
||||
local function checkStreamStatus (stream)
|
||||
-- check if the stream is linked to a bluetooth loopback source
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
local peer_id = lutils.getNodePeerId (stream_id)
|
||||
if peer_id ~= nil then
|
||||
local bt_node = loopback_nodes_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" }
|
||||
}
|
||||
if bt_node ~= nil then
|
||||
local dev_id = bt_node.properties["device.id"]
|
||||
if dev_id ~= nil then
|
||||
-- If a stream we previously saw stops running, we consider it
|
||||
-- inactive, because some applications (Teams) just cork input
|
||||
-- streams, but don't close them.
|
||||
if previous_streams [stream.id] == dev_id and
|
||||
stream.state ~= "running" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return dev_id
|
||||
end
|
||||
else
|
||||
-- Check if it is linked to a filter main node, and recursively advance if so
|
||||
local filter_main_node = filter_nodes_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" }
|
||||
}
|
||||
if filter_main_node ~= nil then
|
||||
-- Now check the all stream nodes for this filter
|
||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||
for filter_stream_node in filter_nodes_om:iterate {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||
} do
|
||||
local dev_id = checkStreamStatus (filter_stream_node)
|
||||
if dev_id ~= nil then
|
||||
return dev_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function handleStream (stream)
|
||||
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||
return
|
||||
end
|
||||
|
||||
local dev_id = checkStreamStatus (stream)
|
||||
if dev_id ~= nil then
|
||||
active_streams [stream.id] = dev_id
|
||||
previous_streams [stream.id] = dev_id
|
||||
switchDeviceToHeadsetProfile (dev_id)
|
||||
else
|
||||
dev_id = active_streams [stream.id]
|
||||
active_streams [stream.id] = nil
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function handleAllStreams ()
|
||||
for stream in streams_om:iterate() do
|
||||
handleStream (stream)
|
||||
end
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node-removed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local stream = event:get_subject ()
|
||||
local dev_id = active_streams[stream.id]
|
||||
active_streams[stream.id] = nil
|
||||
previous_streams[stream.id] = nil
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "link-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "link-added" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local link = event:get_subject ()
|
||||
local p = link.properties
|
||||
for stream in streams_om:iterate () do
|
||||
local in_id = tonumber(p["link.input.node"])
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
if in_id == stream_id then
|
||||
handleStream (stream)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "bluez-device-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-added" },
|
||||
Constraint { "device.api", "=", "bluez5" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
-- Devices are unswitched initially
|
||||
saveLastProfile (device, nil)
|
||||
handleAllStreams ()
|
||||
end
|
||||
}:register ()
|
||||
|
||||
devices_om:activate ()
|
||||
streams_om:activate ()
|
||||
filter_nodes_om:activate ()
|
||||
loopback_nodes_om:activate()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- 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")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/find-best-profile",
|
||||
after = "device/find-preferred-profile",
|
||||
before = "device/apply-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-profile" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local selected_profile = event:get_data ("selected-profile")
|
||||
|
||||
-- skip hook if profile is already selected
|
||||
if selected_profile then
|
||||
return
|
||||
end
|
||||
|
||||
local device = event:get_subject ()
|
||||
local dev_name = device.properties["device.name"] or ""
|
||||
local off_profile = nil
|
||||
local best_profile = nil
|
||||
local unk_profile = nil
|
||||
-- Takes absolute priority if available or unknown
|
||||
local profile_prop = device.properties["device.profile"]
|
||||
|
||||
|
||||
for p in device:iterate_params ("EnumProfile") do
|
||||
profile = cutils.parseParam (p, "EnumProfile")
|
||||
if profile and profile.name == profile_prop and profile.available ~= "no" then
|
||||
selected_profile = profile
|
||||
goto profile_set
|
||||
elseif 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
|
||||
|
||||
::profile_set::
|
||||
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()
|
||||
@@ -0,0 +1,77 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021-2022 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- Based on default-routes.c from pipewire-media-session
|
||||
-- Copyright © 2020 Wim Taymans
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- find the best route for a given device_id, based on availability and priority
|
||||
|
||||
cutils = require ("common-utils")
|
||||
devinfo = require ("device-info-cache")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/find-best-routes",
|
||||
after = "device/find-stored-routes",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-routes" },
|
||||
Constraint { "profile.active-device-ids", "is-present" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local event_properties = event:get_properties ()
|
||||
local active_ids = event_properties ["profile.active-device-ids"]
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
||||
-- active IDs are exchanged in JSON format
|
||||
active_ids = Json.Raw (active_ids):parse ()
|
||||
|
||||
for _, device_id in ipairs (active_ids) do
|
||||
-- if a previous hook already selected a route for this device_id, skip it
|
||||
if selected_routes [tostring (device_id)] then
|
||||
goto next_device_id
|
||||
end
|
||||
|
||||
local best_avail = nil
|
||||
local best_unk = nil
|
||||
for _, ri in pairs (dev_info.route_infos) do
|
||||
if cutils.arrayContains (ri.devices, device_id) and
|
||||
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) then
|
||||
if ri.available == "yes" or ri.available == "unknown" then
|
||||
if ri.direction == "Output" and ri.available ~= ri.prev_available then
|
||||
best_avail = ri
|
||||
ri.save = true
|
||||
break
|
||||
elseif ri.available == "yes" then
|
||||
if (best_avail == nil or ri.priority > best_avail.priority) then
|
||||
best_avail = ri
|
||||
end
|
||||
elseif best_unk == nil or ri.priority > best_unk.priority then
|
||||
best_unk = ri
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local route = best_avail or best_unk
|
||||
if route then
|
||||
selected_routes [tostring (device_id)] =
|
||||
Json.Object { index = route.index }:to_string ()
|
||||
end
|
||||
|
||||
::next_device_id::
|
||||
end
|
||||
|
||||
-- save the selected routes for the apply-routes hook
|
||||
event:set_data ("selected-routes", selected_routes)
|
||||
end
|
||||
}:register ()
|
||||
@@ -0,0 +1,68 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2024 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Finds the user preferred profile for a device, based on the priorities
|
||||
-- defined in the "device.profile.priority.rules" section of the configuration.
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
config = {}
|
||||
config.rules = Conf.get_section_as_json ("device.profile.priority.rules", Json.Array {})
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/find-preferred-profile",
|
||||
after = "device/find-stored-profile",
|
||||
before = "device/find-best-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-profile" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local selected_profile = event:get_data ("selected-profile")
|
||||
|
||||
-- skip hook if the profile is already selected for this device.
|
||||
if selected_profile then
|
||||
return
|
||||
end
|
||||
|
||||
local device = event:get_subject ()
|
||||
local props = JsonUtils.match_rules_update_properties (
|
||||
config.rules, device.properties)
|
||||
local p_array = props["priorities"]
|
||||
|
||||
-- skip hook if the profile priorities are NOT defined for this device.
|
||||
if not p_array then
|
||||
return nil
|
||||
end
|
||||
|
||||
local p_json = Json.Raw(p_array)
|
||||
local priorities = p_json:parse()
|
||||
|
||||
for _, priority_profile in ipairs(priorities) do
|
||||
for p in device:iterate_params("EnumProfile") do
|
||||
local device_profile = cutils.parseParam(p, "EnumProfile")
|
||||
if device_profile and device_profile.name == priority_profile then
|
||||
selected_profile = device_profile
|
||||
goto profile_set
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::profile_set::
|
||||
if selected_profile then
|
||||
log:info (device, string.format (
|
||||
"Found preferred profile '%s' (%d) for device '%s'",
|
||||
selected_profile.name, selected_profile.index, device_name))
|
||||
event:set_data ("selected-profile", selected_profile)
|
||||
else
|
||||
log:info (device, "Profiles listed in 'device.profile.priority.rules'"
|
||||
.. " do not match the available ones of device: " .. device_name)
|
||||
end
|
||||
|
||||
end
|
||||
}:register()
|
||||
@@ -0,0 +1,28 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- look for new devices and raise select-profile event.
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/select-profile",
|
||||
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()
|
||||
160
pipewire/.config/wireplumber/scripts/device/select-routes.lua
Normal file
160
pipewire/.config/wireplumber/scripts/device/select-routes.lua
Normal file
@@ -0,0 +1,160 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021-2022 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- Based on default-routes.c from pipewire-media-session
|
||||
-- Copyright © 2020 Wim Taymans
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Update the device info cache with the latest information from EnumRoute(all
|
||||
-- the device routes) and trigger a "select-routes" event to select new routes
|
||||
-- for the given device configuration, if it has changed
|
||||
|
||||
cutils = require ("common-utils")
|
||||
devinfo = require ("device-info-cache")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/select-route",
|
||||
after = "device/select-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-added" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "c", "EnumRoute" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local device = event:get_subject ()
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
if not dev_info then
|
||||
return
|
||||
end
|
||||
|
||||
local new_route_infos = {}
|
||||
local avail_routes_changed = false
|
||||
local profile = nil
|
||||
|
||||
-- get current profile
|
||||
for p in device:iterate_params ("Profile") do
|
||||
profile = cutils.parseParam (p, "Profile")
|
||||
end
|
||||
|
||||
-- look at all the routes and update/reset cached information
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
-- parse pod
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
-- find cached route information
|
||||
local route_info = devinfo.find_route_info (dev_info, route, true)
|
||||
|
||||
-- update properties
|
||||
route_info.prev_available = route_info.available
|
||||
if route_info.available ~= route.available then
|
||||
log:info (device, "route " .. route.name .. " available changed " ..
|
||||
route_info.available .. " -> " .. route.available)
|
||||
route_info.available = route.available
|
||||
if profile and cutils.arrayContains (route.profiles, profile.index) then
|
||||
avail_routes_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
-- store
|
||||
new_route_infos [route.index] = route_info
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
|
||||
-- replace old route_infos to lose old routes
|
||||
-- that no longer exist on the device
|
||||
dev_info.route_infos = new_route_infos
|
||||
new_route_infos = nil
|
||||
|
||||
-- restore routes for profile
|
||||
if profile then
|
||||
local profile_changed = (dev_info.active_profile ~= profile.index)
|
||||
dev_info.active_profile = profile.index
|
||||
|
||||
-- if the profile changed, restore routes for that profile
|
||||
-- if any of the routes of the current profile changed in availability,
|
||||
-- then try to select a new "best" route for each device and ignore
|
||||
-- what was stored
|
||||
if profile_changed or avail_routes_changed then
|
||||
log:info (device,
|
||||
string.format ("restore routes for profile(%s) of device(%s)",
|
||||
profile.name, dev_info.name))
|
||||
|
||||
-- find the active device IDs for which to select routes
|
||||
local active_ids = findActiveDeviceIDs (profile)
|
||||
active_ids = Json.Array (active_ids):to_string ()
|
||||
|
||||
-- push select-routes event and let the hooks select the appropriate routes
|
||||
local props = {
|
||||
["profile.changed"] = profile_changed,
|
||||
["profile.name"] = profile.name,
|
||||
["profile.active-device-ids"] = active_ids,
|
||||
}
|
||||
source:call ("push-event", "select-routes", device, props)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register()
|
||||
|
||||
-- These device ids are like routes(speaker, mic, headset etc) or sub-devices or
|
||||
-- paths with in the pipewire devices/soundcards.
|
||||
function findActiveDeviceIDs (profile)
|
||||
-- parses the classes from the profile and returns the device IDs
|
||||
----- sample structure, should return { 0, 8 } -----
|
||||
-- classes:
|
||||
-- 1: 2
|
||||
-- 2:
|
||||
-- 1: Audio/Source
|
||||
-- 2: 1
|
||||
-- 3: card.profile.devices
|
||||
-- 4:
|
||||
-- 1: 0
|
||||
-- pod_type: Array
|
||||
-- value_type: Spa:Int
|
||||
-- pod_type: Struct
|
||||
-- 3:
|
||||
-- 1: Audio/Sink
|
||||
-- 2: 1
|
||||
-- 3: card.profile.devices
|
||||
-- 4:
|
||||
-- 1: 8
|
||||
-- pod_type: Array
|
||||
-- value_type: Spa:Int
|
||||
-- pod_type: Struct
|
||||
-- pod_type: Struct
|
||||
local active_ids = {}
|
||||
if type (profile.classes) == "table" and profile.classes.pod_type == "Struct" then
|
||||
for _, p in ipairs (profile.classes) do
|
||||
if type (p) == "table" and p.pod_type == "Struct" then
|
||||
local i = 1
|
||||
while true do
|
||||
local k, v = p [i], p [i+1]
|
||||
i = i + 2
|
||||
if not k or not v then
|
||||
break
|
||||
end
|
||||
if k == "card.profile.devices" and
|
||||
type (v) == "table" and v.pod_type == "Array" then
|
||||
for _, dev_id in ipairs (v) do
|
||||
table.insert (active_ids, dev_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return active_ids
|
||||
end
|
||||
145
pipewire/.config/wireplumber/scripts/device/state-profile.lua
Normal file
145
pipewire/.config/wireplumber/scripts/device/state-profile.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
-- 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.
|
||||
|
||||
-- A devices profile needs to be selected for any new device. the script selects
|
||||
-- the device profile from the user preferences, as well as store the user
|
||||
-- selected device profile to state file
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
-- the state storage
|
||||
state = nil
|
||||
state_table = nil
|
||||
|
||||
find_stored_profile_hook = SimpleEventHook {
|
||||
name = "device/find-stored-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-profile" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local selected_profile = event:get_data ("selected-profile")
|
||||
|
||||
-- skip hook if profile is already selected
|
||||
if selected_profile then
|
||||
return
|
||||
end
|
||||
|
||||
local device = event:get_subject ()
|
||||
local dev_name = device.properties["device.name"]
|
||||
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 and profile.available ~= "no" 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
|
||||
}
|
||||
|
||||
store_user_selected_profile_hook = SimpleEventHook {
|
||||
name = "device/store-user-selected-profile",
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
state:save_after_timeout (state_table)
|
||||
|
||||
log:info (device, string.format (
|
||||
"stored profile '%s' (%d) for device '%s'",
|
||||
profile.name, index, dev_name))
|
||||
end
|
||||
|
||||
function toggleState (enable)
|
||||
if enable and not state then
|
||||
state = State ("default-profile")
|
||||
state_table = state:load ()
|
||||
find_stored_profile_hook:register ()
|
||||
store_user_selected_profile_hook:register ()
|
||||
elseif not enable and state then
|
||||
state = nil
|
||||
state_table = nil
|
||||
find_stored_profile_hook:remove ()
|
||||
store_user_selected_profile_hook:remove ()
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("device.restore-profile", function ()
|
||||
toggleState (Settings.get_boolean ("device.restore-profile"))
|
||||
end)
|
||||
toggleState (Settings.get_boolean ("device.restore-profile"))
|
||||
345
pipewire/.config/wireplumber/scripts/device/state-routes.lua
Normal file
345
pipewire/.config/wireplumber/scripts/device/state-routes.lua
Normal file
@@ -0,0 +1,345 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021-2022 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- Based on default-routes.c from pipewire-media-session
|
||||
-- Copyright © 2020 Wim Taymans
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- This file contains all the logic related to saving device routes and their
|
||||
-- properties to a state file and restoring both the routes selection and
|
||||
-- the properties of routes later on.
|
||||
--
|
||||
|
||||
cutils = require ("common-utils")
|
||||
devinfo = require ("device-info-cache")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
-- the state storage
|
||||
state = nil
|
||||
state_table = nil
|
||||
|
||||
-- hook to restore routes selection for a newly selected profile
|
||||
find_stored_routes_hook = SimpleEventHook {
|
||||
name = "device/find-stored-routes",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-routes" },
|
||||
Constraint { "profile.changed", "=", "true" },
|
||||
Constraint { "profile.active-device-ids", "is-present" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local event_properties = event:get_properties ()
|
||||
local profile_name = event_properties ["profile.name"]
|
||||
local active_ids = event_properties ["profile.active-device-ids"]
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
||||
-- get the stored routes for this profile
|
||||
-- skip the hook if there are no stored routes, there is no point
|
||||
local spr = getStoredProfileRoutes (dev_info, profile_name)
|
||||
if #spr == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- active IDs are exchanged in JSON format
|
||||
active_ids = Json.Raw (active_ids):parse ()
|
||||
|
||||
for _, device_id in ipairs (active_ids) do
|
||||
-- if a previous hook already selected a route for this device_id, skip it
|
||||
if selected_routes [tostring (device_id)] then
|
||||
goto next_device_id
|
||||
end
|
||||
|
||||
log:info (device, "restoring route for device ID " .. tostring (device_id));
|
||||
|
||||
local route_info = nil
|
||||
|
||||
-- find a route that was previously stored for a device_id
|
||||
for _, ri in pairs (dev_info.route_infos) do
|
||||
if cutils.arrayContains (ri.devices, tonumber (device_id)) and
|
||||
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) and
|
||||
cutils.arrayContains (spr, ri.name) then
|
||||
route_info = ri
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if route_info then
|
||||
-- we found a stored route
|
||||
if route_info.available == "no" then
|
||||
log:info (device, "stored route '" .. route_info.name .. "' not available")
|
||||
-- not available, try to find next best
|
||||
route_info = nil
|
||||
else
|
||||
log:info (device, "found stored route: " .. route_info.name)
|
||||
-- make sure we save it again
|
||||
route_info.save = true
|
||||
end
|
||||
end
|
||||
|
||||
if route_info then
|
||||
selected_routes [tostring (device_id)] =
|
||||
Json.Object { index = route_info.index }:to_string ()
|
||||
end
|
||||
|
||||
::next_device_id::
|
||||
end
|
||||
|
||||
-- save the selected routes for the apply-routes hook
|
||||
event:set_data ("selected-routes", selected_routes)
|
||||
end
|
||||
}
|
||||
|
||||
-- extract the "selected-routes" event data and augment it to include
|
||||
-- the route properties, as they were stored in the state file;
|
||||
-- this is the last step before applying the routes
|
||||
apply_route_props_hook = SimpleEventHook {
|
||||
name = "device/apply-route-props",
|
||||
after = { "device/find-stored-routes", "device/find-best-routes" },
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-routes" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
local new_selected_routes = {}
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
||||
if next (selected_routes) == nil then
|
||||
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||
return
|
||||
end
|
||||
|
||||
for device_id, route in pairs (selected_routes) do
|
||||
-- JSON to lua table
|
||||
route = Json.Raw (route):parse ()
|
||||
|
||||
local route_info = devinfo.find_route_info (dev_info, route, false)
|
||||
local props = getStoredRouteProps (dev_info, route_info)
|
||||
|
||||
-- convert arrays to Json
|
||||
if props.channelVolumes then
|
||||
props.channelVolumes = Json.Array (props.channelVolumes)
|
||||
end
|
||||
if props.channelMap then
|
||||
props.channelMap = Json.Array (props.channelMap)
|
||||
end
|
||||
if props.iec958Codecs then
|
||||
props.iec958Codecs = Json.Array (props.iec958Codecs)
|
||||
end
|
||||
|
||||
local json = Json.Object {
|
||||
index = route_info.index,
|
||||
props = Json.Object (props),
|
||||
}
|
||||
new_selected_routes [device_id] = json:to_string ()
|
||||
end
|
||||
|
||||
-- save the selected routes for the apply-routes hook
|
||||
event:set_data ("selected-routes", new_selected_routes)
|
||||
end
|
||||
}
|
||||
|
||||
store_or_restore_routes_hook = SimpleEventHook {
|
||||
name = "device/store-or-restore-routes",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Route" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local selected_routes = {}
|
||||
local push_select_routes = false
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
if not dev_info then
|
||||
return
|
||||
end
|
||||
|
||||
local new_route_infos = {}
|
||||
|
||||
-- look at all the routes and update/reset cached information
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
-- parse pod
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
-- find cached route information
|
||||
local route_info = devinfo.find_route_info (dev_info, route, true)
|
||||
if not route_info then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
-- update properties
|
||||
route_info.prev_active = route_info.active
|
||||
route_info.active = false
|
||||
route_info.save = false
|
||||
|
||||
-- store
|
||||
new_route_infos [route.index] = route_info
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
|
||||
-- update route_infos with new prev_active, active and save changes
|
||||
dev_info.route_infos = new_route_infos
|
||||
new_route_infos = nil
|
||||
|
||||
-- check for changes in the active routes
|
||||
for p in device:iterate_params ("Route") do
|
||||
local route = cutils.parseParam (p, "Route")
|
||||
if not route then
|
||||
goto skip_route
|
||||
end
|
||||
|
||||
-- get cached route info and at the same time
|
||||
-- ensure that the route is also in EnumRoute
|
||||
local route_info = devinfo.find_route_info (dev_info, route, false)
|
||||
if not route_info then
|
||||
goto skip_route
|
||||
end
|
||||
|
||||
-- update route_info state
|
||||
route_info.active = true
|
||||
route_info.save = route.save
|
||||
|
||||
if not route_info.prev_active then
|
||||
-- a new route is now active, restore the volume and
|
||||
-- make sure we save this as a preferred route
|
||||
log:info (device,
|
||||
string.format ("new active route(%s) found of device(%s)",
|
||||
route.name, dev_info.name))
|
||||
route_info.prev_active = true
|
||||
route_info.active = true
|
||||
|
||||
selected_routes [tostring (route.device)] =
|
||||
Json.Object { index = route_info.index }:to_string ()
|
||||
push_select_routes = true
|
||||
|
||||
elseif route.save and route.props then
|
||||
-- just save route properties
|
||||
log:info (device,
|
||||
string.format ("storing route(%s) props of device(%s)",
|
||||
route.name, dev_info.name))
|
||||
|
||||
saveRouteProps (dev_info, route)
|
||||
end
|
||||
|
||||
::skip_route::
|
||||
end
|
||||
|
||||
-- save selected routes for the active profile
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
saveProfileRoutes (dev_info, profile.name)
|
||||
end
|
||||
|
||||
-- push a select-routes event to re-apply the routes with new properties
|
||||
if push_select_routes then
|
||||
local e = source:call ("create-event", "select-routes", device, nil)
|
||||
e:set_data ("selected-routes", selected_routes)
|
||||
EventDispatcher.push_event (e)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function saveRouteProps (dev_info, route)
|
||||
local props = route.props.properties
|
||||
local key = dev_info.name .. ":" ..
|
||||
route.direction:lower () .. ":" ..
|
||||
route.name
|
||||
|
||||
state_table [key] = Json.Object {
|
||||
volume = props.volume,
|
||||
mute = props.mute,
|
||||
channelVolumes = props.channelVolumes and Json.Array (props.channelVolumes),
|
||||
channelMap = props.channelMap and Json.Array (props.channelMap),
|
||||
latencyOffsetNsec = props.latencyOffsetNsec,
|
||||
iec958Codecs = props.iec958Codecs and Json.Array (props.iec958Codecs),
|
||||
}:to_string ()
|
||||
|
||||
state:save_after_timeout (state_table)
|
||||
end
|
||||
|
||||
function getStoredRouteProps (dev_info, route)
|
||||
local key = dev_info.name .. ":" ..
|
||||
route.direction:lower () .. ":" ..
|
||||
route.name
|
||||
local value = state_table [key]
|
||||
if value then
|
||||
local json = Json.Raw (value)
|
||||
if json and json:is_object () then
|
||||
return json:parse ()
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
-- stores an array with the route names that are selected
|
||||
-- for the given device and profile
|
||||
function saveProfileRoutes (dev_info, profile_name)
|
||||
-- select only routes with save == true
|
||||
local routes = {}
|
||||
for idx, ri in pairs (dev_info.route_infos) do
|
||||
if ri.save then
|
||||
table.insert (routes, ri.name)
|
||||
end
|
||||
end
|
||||
|
||||
if #routes > 0 then
|
||||
local key = dev_info.name .. ":profile:" .. profile_name
|
||||
state_table [key] = Json.Array (routes):to_string()
|
||||
state:save_after_timeout (state_table)
|
||||
end
|
||||
end
|
||||
|
||||
-- returns an array of the route names that were previously selected
|
||||
-- for the given device and profile
|
||||
function getStoredProfileRoutes (dev_info, profile_name)
|
||||
local key = dev_info.name .. ":profile:" .. profile_name
|
||||
local value = state_table [key]
|
||||
if value then
|
||||
local json = Json.Raw (value)
|
||||
if json and json:is_array () then
|
||||
return json:parse ()
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
function toggleState (enable)
|
||||
if enable and not state then
|
||||
state = State ("default-routes")
|
||||
state_table = state:load ()
|
||||
find_stored_routes_hook:register ()
|
||||
apply_route_props_hook:register ()
|
||||
store_or_restore_routes_hook:register ()
|
||||
elseif not enable and state then
|
||||
state = nil
|
||||
state_table = nil
|
||||
find_stored_routes_hook:remove ()
|
||||
apply_route_props_hook:remove ()
|
||||
store_or_restore_routes_hook:remove ()
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("device.restore-routes", function ()
|
||||
toggleState (Settings.get_boolean ("device.restore-routes"))
|
||||
end)
|
||||
toggleState (Settings.get_boolean ("device.restore-routes"))
|
||||
Reference in New Issue
Block a user