%PDF- %PDF-
Direktori : /usr/share/wireplumber/scripts/monitors/ |
Current File : //usr/share/wireplumber/scripts/monitors/bluez.lua |
-- WirePlumber -- -- Copyright © 2021 Collabora Ltd. -- @author George Kiagiadakis <george.kiagiadakis@collabora.com> -- -- SPDX-License-Identifier: MIT local config = ... or {} local COMBINE_OFFSET = 64 devices_om = ObjectManager { Interest { type = "device", } } nodes_om = ObjectManager { Interest { type = "node", Constraint { "node.name", "#", "*.bluez_*put*"}, Constraint { "device.id", "+" }, } } -- preprocess rules and create Interest objects for _, r in ipairs(config.rules or {}) do r.interests = {} for _, i in ipairs(r.matches) do local interest_desc = { type = "properties" } for _, c in ipairs(i) do c.type = "pw" table.insert(interest_desc, Constraint(c)) end local interest = Interest(interest_desc) table.insert(r.interests, interest) end r.matches = nil end -- applies properties from config.rules when asked to function rulesApplyProperties(properties) for _, r in ipairs(config.rules or {}) do if r.apply_properties then for _, interest in ipairs(r.interests) do if interest:matches(properties) then for k, v in pairs(r.apply_properties) do properties[k] = v end end end end end end function setOffloadActive(device, value) local pod = Pod.Object { "Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value } device:set_params("Props", pod) end nodes_om:connect("object-added", function(_, node) node:connect("state-changed", function(node, old_state, cur_state) local interest = Interest { type = "device", Constraint { "object.id", "=", node.properties["device.id"]} } for d in devices_om:iterate (interest) do if cur_state == "running" then setOffloadActive(d, true) else setOffloadActive(d, false) end end end) end) function createOffloadScoNode(parent, id, type, factory, properties) local dev_props = parent.properties local args = { ["audio.channels"] = 1, ["audio.position"] = "[MONO]", } local desc = dev_props["device.description"] or dev_props["device.name"] or dev_props["device.nick"] or dev_props["device.alias"] or "bluetooth-device" -- sanitize description, replace ':' with ' ' args["node.description"] = desc:gsub("(:)", " ") if factory:find("sink") then local capture_args = { ["device.id"] = parent["bound-id"], ["media.class"] = "Audio/Sink", ["node.pause-on-idle"] = false, } for k, v in pairs(properties) do capture_args[k] = v end local name = "bluez_output" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id) args["node.name"] = name:gsub("([^%w_%-%.])", "_") args["capture.props"] = Json.Object(capture_args) args["playback.props"] = Json.Object { ["node.passive"] = true, ["node.pause-on-idle"] = false, } elseif factory:find("source") then local playback_args = { ["device.id"] = parent["bound-id"], ["media.class"] = "Audio/Source", ["node.pause-on-idle"] = false, } for k, v in pairs(properties) do playback_args[k] = v end local name = "bluez_input" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id) args["node.name"] = name:gsub("([^%w_%-%.])", "_") args["capture.props"] = Json.Object { ["node.passive"] = true, ["node.pause-on-idle"] = false, } args["playback.props"] = Json.Object(playback_args) else Log.warning(parent, "Unsupported factory: " .. factory) return end -- Transform 'args' to a json object here local args_json = Json.Object(args) -- and get the final JSON as a string from the json object local args_string = args_json:get_data() local loopback_properties = {} local loopback = LocalModule("libpipewire-module-loopback", args_string, loopback_properties) parent:store_managed_object(id, loopback) end device_set_nodes_om = ObjectManager { Interest { type = "node", Constraint { "api.bluez5.set.leader", "+", type = "pw" }, } } device_set_nodes_om:connect ("object-added", function(_, node) -- Connect ObjectConfig events to the right node if not monitor then return end local interest = Interest { type = "device", Constraint { "object.id", "=", node.properties["device.id"] } } Log.info("Device set node found: " .. tostring (node["bound-id"])) for device in devices_om:iterate (interest) do local device_id = device.properties["api.bluez5.id"] if not device_id then goto next_device end local spa_device = monitor:get_managed_object (tonumber (device_id)) if not spa_device then goto next_device end local id = node.properties["card.profile.device"] if id ~= nil then Log.info(".. assign to device: " .. tostring (device["bound-id"]) .. " node " .. tostring (id)) spa_device:store_managed_object (id, node) -- set routes again to update volumes etc. for route in device:iterate_params ("Route") do device:set_param ("Route", route) end end ::next_device:: end end) function createSetNode(parent, id, type, factory, properties) local args = {} local name local target_class local stream_class local rules = {} local members_json = Json.Raw (properties["api.bluez5.set.members"]) local channels_json = Json.Raw (properties["api.bluez5.set.channels"]) local members = members_json:parse () local channels = channels_json:parse () if properties["media.class"] == "Audio/Sink" then name = "bluez_output" args["combine.mode"] = "sink" target_class = "Audio/Sink/Internal" stream_class = "Stream/Output/Audio/Internal" else name = "bluez_input" args["combine.mode"] = "source" target_class = "Audio/Source/Internal" stream_class = "Stream/Input/Audio/Internal" end Log.info("Device set: " .. properties["node.name"]) for _, member in pairs(members) do Log.info("Device set member:" .. member["object.path"]) table.insert(rules, Json.Object { ["matches"] = Json.Array { Json.Object { ["object.path"] = member["object.path"], ["media.class"] = target_class, }, }, ["actions"] = Json.Object { ["create-stream"] = Json.Object { ["media.class"] = stream_class, ["audio.position"] = Json.Array (member["channels"]), } }, } ) end properties["node.virtual"] = false properties["device.api"] = "bluez5" properties["api.bluez5.set.members"] = nil properties["api.bluez5.set.channels"] = nil properties["api.bluez5.set.leader"] = true properties["audio.position"] = Json.Array (channels) args["combine.props"] = Json.Object (properties) args["stream.props"] = Json.Object {} args["stream.rules"] = Json.Array (rules) local args_json = Json.Object(args) local args_string = args_json:get_data() local combine_properties = {} Log.info("Device set node: " .. args_string) return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties) end function createNode(parent, id, type, factory, properties) local dev_props = parent.properties if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then createOffloadScoNode(parent, id, type, factory, properties) return end -- set the device id and spa factory name; REQUIRED, do not change properties["device.id"] = parent["bound-id"] properties["factory.name"] = factory -- set the default pause-on-idle setting properties["node.pause-on-idle"] = false -- set the node description local desc = dev_props["device.description"] or dev_props["device.name"] or dev_props["device.nick"] or dev_props["device.alias"] or "bluetooth-device" -- sanitize description, replace ':' with ' ' properties["node.description"] = desc:gsub("(:)", " ") -- set the node name local name = ((factory:find("sink") and "bluez_output") or (factory:find("source") and "bluez_input" or factory)) .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id) -- sanitize name properties["node.name"] = name:gsub("([^%w_%-%.])", "_") -- set priority if not properties["priority.driver"] then local priority = factory:find("source") and 2010 or 1010 properties["priority.driver"] = priority properties["priority.session"] = priority end -- autoconnect if it's a stream if properties["api.bluez5.profile"] == "headset-audio-gateway" or properties["api.bluez5.profile"] == "bap-sink" or factory:find("a2dp.source") or factory:find("media.source") then properties["node.autoconnect"] = true end -- apply properties from config.rules rulesApplyProperties(properties) -- create the node; bluez requires "local" nodes, i.e. ones that run in -- the same process as the spa device, for several reasons if properties["api.bluez5.set.leader"] then local combine = createSetNode(parent, id, type, factory, properties) parent:store_managed_object(id + COMBINE_OFFSET, combine) else local node = LocalNode("adapter", properties) node:activate(Feature.Proxy.BOUND) parent:store_managed_object(id, node) end end function removeNode(parent, id) -- Clear also the device set module, if any parent:store_managed_object(id + COMBINE_OFFSET, nil) end function createDevice(parent, id, type, factory, properties) local device = parent:get_managed_object(id) if not device then -- ensure a proper device name local name = (properties["device.name"] or properties["api.bluez5.address"] or properties["device.description"] or tostring(id)):gsub("([^%w_%-%.])", "_") if not name:find("^bluez_card%.", 1) then name = "bluez_card." .. name end properties["device.name"] = name -- set the icon name if not properties["device.icon-name"] then local icon = nil local icon_map = { -- form factor -> icon ["microphone"] = "audio-input-microphone", ["webcam"] = "camera-web", ["handset"] = "phone", ["portable"] = "multimedia-player", ["tv"] = "video-display", ["headset"] = "audio-headset", ["headphone"] = "audio-headphones", ["speaker"] = "audio-speakers", ["hands-free"] = "audio-handsfree", } local f = properties["device.form-factor"] local b = properties["device.bus"] icon = icon_map[f] or "audio-card" properties["device.icon-name"] = icon .. (b and ("-" .. b) or "") end -- initial profile is to be set by policy-device-profile.lua, not spa-bluez5 properties["bluez5.profile"] = "off" properties["api.bluez5.id"] = id -- apply properties from config.rules rulesApplyProperties(properties) -- create the device device = SpaDevice(factory, properties) if device then device:connect("create-object", createNode) device:connect("object-removed", removeNode) parent:store_managed_object(id, device) else Log.warning ("Failed to create '" .. factory .. "' device") return end end Log.info(parent, string.format("%d, %s (%s): %s", id, properties["device.description"], properties["api.bluez5.address"], properties["api.bluez5.connection"])) -- activate the device after the bluez profiles are connected if properties["api.bluez5.connection"] == "connected" then device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) else device:deactivate(Features.ALL) end end function createMonitor() local monitor_props = config.properties or {} monitor_props["api.bluez5.connection-info"] = true local monitor = SpaDevice("api.bluez5.enum.dbus", monitor_props) if monitor then monitor:connect("create-object", createDevice) else Log.message("PipeWire's BlueZ SPA missing or broken. Bluetooth not supported.") return nil end monitor:activate(Feature.SpaDevice.ENABLED) return monitor end logind_plugin = Plugin.find("logind") if logind_plugin then -- if logind support is enabled, activate -- the monitor only when the seat is active function startStopMonitor(seat_state) Log.info(logind_plugin, "Seat state changed: " .. seat_state) if seat_state == "active" then monitor = createMonitor() elseif monitor then monitor:deactivate(Feature.SpaDevice.ENABLED) monitor = nil end end logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end) startStopMonitor(logind_plugin:call("get-state")) else monitor = createMonitor() end nodes_om:activate() devices_om:activate() device_set_nodes_om:activate()