diff --git a/src/config/main.lua.d/30-libcamera-monitor.lua b/src/config/main.lua.d/30-libcamera-monitor.lua new file mode 100644 index 00000000..eda92573 --- /dev/null +++ b/src/config/main.lua.d/30-libcamera-monitor.lua @@ -0,0 +1,10 @@ +libcamera_monitor = {} +libcamera_monitor.properties = {} +libcamera_monitor.rules = {} + +function libcamera_monitor.enable() + load_monitor("libcamera", { + properties = libcamera_monitor.properties, + rules = libcamera_monitor.rules, + }) +end diff --git a/src/config/main.lua.d/50-libcamera-config.lua b/src/config/main.lua.d/50-libcamera-config.lua new file mode 100644 index 00000000..f70a4608 --- /dev/null +++ b/src/config/main.lua.d/50-libcamera-config.lua @@ -0,0 +1,37 @@ + +libcamera_monitor.rules = { + -- An array of matches/actions to evaluate. + { + -- 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", "matches", "libcamera_device.*" }, + }, + }, + -- Apply properties on the matched object. + apply_properties = { + -- ["device.nick"] = "My Device", + }, + }, + { + matches = { + { + -- Matches all sources. + { "node.name", "matches", "libcamera_input.*" }, + }, + { + -- Matches all sinks. + { "node.name", "matches", "libcamera_output.*" }, + }, + }, + apply_properties = { + --["node.nick"] = "My Node", + --["priority.driver"] = 100, + --["priority.session"] = 100, + --["node.pause-on-idle"] = false, + }, + }, +} diff --git a/src/scripts/monitors/libcamera.lua b/src/scripts/monitors/libcamera.lua new file mode 100644 index 00000000..7209c620 --- /dev/null +++ b/src/scripts/monitors/libcamera.lua @@ -0,0 +1,139 @@ +-- WirePlumber +-- +-- Copyright © 2021 Collabora Ltd. +-- @author George Kiagiadakis +-- +-- SPDX-License-Identifier: MIT + +local config = ... or {} + +-- 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 findDuplicate(parent, id, property, value) + for i = 0, id - 1, 1 do + local obj = parent:get_managed_object(i) + if obj and obj.properties[property] == value then + return true + end + end + return false +end + +function createNode(parent, id, type, factory, properties) + local dev_props = parent.properties + + -- 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 name + local name = + (factory:find("sink") and "libcamera_output") or + (factory:find("source") and "libcamera_input" or factory) + .. "." .. + (dev_props["device.name"]:gsub("^libcamera_device%.(.+)", "%1") or + dev_props["device.name"] or + dev_props["device.nick"] or + dev_props["device.alias"] or + "libcamera-device") + -- sanitize name + name = name:gsub("([^%w_%-%.])", "_") + + properties["node.name"] = name + + -- deduplicate nodes with the same name + for counter = 2, 99, 1 do + if findDuplicate(parent, id, "node.name", properties["node.name"]) then + properties["node.name"] = name .. "." .. counter + else + break + end + end + + -- set the node description + local desc = dev_props["device.description"] or "libcamera-device" + -- sanitize description, replace ':' with ' ' + properties["node.description"] = desc:gsub("(:)", " ") + + -- apply properties from config.rules + rulesApplyProperties(properties) + + -- create the node + local node = Node("spa-node-factory", properties) + node:activate(Feature.Proxy.BOUND) + parent:store_managed_object(id, node) +end + +function createDevice(parent, id, type, factory, properties) + -- ensure the device has an appropriate name + local name = "libcamera_device." .. + (properties["device.name"] or + properties["device.bus-id"] or + properties["device.bus-path"] or + tostring(id)):gsub("([^%w_%-%.])", "_") + + properties["device.name"] = name + + -- deduplicate devices with the same name + for counter = 2, 99, 1 do + if findDuplicate(parent, id, "device.name", properties["device.name"]) then + properties["device.name"] = name .. "." .. counter + else + break + end + end + + -- ensure the device has a description + properties["device.description"] = + properties["device.description"] + or properties["device.product.name"] + or "Unknown device" + + -- apply properties from config.rules + rulesApplyProperties(properties) + + -- create the device + local device = SpaDevice(factory, properties) + device:connect("create-object", createNode) + device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) + parent:store_managed_object(id, device) +end + +monitor = SpaDevice("api.libcamera.enum.client", config.properties or {}) +if monitor then + monitor:connect("create-object", createDevice) + monitor:activate(Feature.SpaDevice.ENABLED) +else + Log.message("PipeWire's libcamera SPA missing or broken. libcamera not supported.") +end