pipewire-0/pw-access.el

264 lines
10 KiB
EmacsLisp

;;; pw-access.el --- PipeWire access -*- lexical-binding: t -*-
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
;; Author: Milan Zamazal <pdm@zamazal.org>
;; Package-Version: 1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: multimedia
;; URL: https://git.zamazal.org/pdm/pipewire-0
;; COPYRIGHT NOTICE
;;
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; A generic interface for communication with PipeWire (https://pipewire.org).
;; It abstracts communication with PipeWire to be backend independent.
;; Only functions from this module may communicate with PipeWire.
;;
;; Currently, pw-cli is used to talk to PipeWire. This is not optimal
;; because pw-cli doesn't seem to have documented output format and
;; the format changes accross PipeWire versions. But there seem to be
;; no better options currently. This module should allow switching to
;; other communication means easily, without any changes needed
;; outside this module, except for using a different communication
;; class.
(require 'eieio)
(defclass pw-accessor ()
()
:documentation
"Base PipeWire interface class.
All PipeWire interfaces should derive from this class.")
(cl-defgeneric pw-access-objects (class)
"Return all the objects currently reported by PipeWire.
It is a list of object data. Each of the elements has a form
\(OBJECT-ID . INFO) where OBJECT-ID is a numeric OBJECT-ID as
reported by PipeWire and INFO is an association list of items
\(NAME . VALUE) where NAME is a string item name as reported by
PipeWire and VALUE is the corresponding value. VALUE is a number for
object ids, a string otherwise.
A special entry with `type' symbol as its name contains the PipeWire
type of the objects, as a string (e.g. \"Device\", \"Node\", \"Port\",
\"Client\", ...).")
(cl-defgeneric pw-access-properties (class node-id)
"Return properties of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
Object properties may be, unlike object info items, settable.
An assocation list is returned. Each list element is of the form
\(PROPERTY . VALUE) where PROPERTY is a string name of the given
property. VALUE can be:
- \"true\" or \"false\" for boolean values (t and nil are not used to
avoid confusion with nil representing invalid or unavailable value).
- A number for numeric values (ids, integers, floats).
- A string for string values.
- A list of elements of any of these types for arrays and structs.")
(cl-defgeneric pw-access-set-properties (class node-id properties)
"Set PROPERTIES of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
PROPERTIES is an association list in the same format as in
`pw-access-properties'. It needn't contain all the properties, just
the properties to be changed.")
(cl-defgeneric pw-access-current-profile (class device-id)
"Return current profile of the given device.
DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire
objects are not supported in this method).
The profile is an association list with elements of the form
\(PROPERTY . VALUE), in the same format as properties in
`pw-access-properties'.")
(cl-defgeneric pw-access-profiles (class device-id)
"Return available profiles of the given device.
DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire
objects are not supported in this method).
Return a list of profiles, which are in the same format as in
`pw-access-current-profile'.")
(cl-defgeneric pw-access-set-profile (class device-id profile-index)
"Set the profile of the given device.
DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire
objects are not supported in this method).
PROFILE-INDEX is a numeric index of the profile to set, as returned
from PipeWire.")
(cl-defgeneric pw-access-defaults (class)
"Return default sinks and sources.
An association lists is returned. Each list element is of the form
\(KEY . NAME) where KEY is a string identifying the given kind of
default sink or source as reported by PipeWire and NAME is a string
name of the node assigned to the default.")
(cl-defgeneric pw-access-set-default (class key node-name)
"Set default sink or source.
KEY is a string identifying the given kind of default sink or source
as reported in `pw-access-defaults' and NODE-NAME is a string name of
the node that should be assigned to KEY.")
;; pw-cli interface
(defvar pw-cli-command "pw-cli"
"pw-cli command to use.")
(defvar pw-cli-metadata-command "pw-metadata"
"pw-metadata command to use.")
(defclass pw-cli-accessor (pw-accessor)
()
:documentation
"pw-cli based interface to PipeWire.
Note this interface may not work with all PipeWire versions.")
(defun pw-cli--command (command args)
(apply #'call-process command nil t nil args)
(goto-char (point-min)))
(defun pw-cli--next-line ()
(goto-char (line-beginning-position 2)))
(defun pw-cli--parse-list ()
(let ((objects '()))
(while (re-search-forward "^[\t]id \\([0-9]+\\), type PipeWire:Interface:\\(.*\\)/.*$" nil t)
(let ((id (string-to-number (match-string 1)))
(properties `((type . ,(match-string 2)))))
(pw-cli--next-line)
(while (looking-at "^ [\t][\t]\\([a-z.]+\\) = \"\\(.*\\)\"")
(let ((property (match-string 1))
(value (match-string 2)))
(when (string-suffix-p ".id" property)
(setq value (string-to-number value)))
(push (cons property value) properties))
(pw-cli--next-line))
(push (cons id properties) objects)))
(nreverse objects)))
(cl-defmethod pw-access-objects ((_class pw-cli-accessor))
(with-temp-buffer
(pw-cli--command pw-cli-command '("list-objects"))
(pw-cli--parse-list)))
(defun pw-cli--read-property (&optional nesting)
(unless nesting
(setq nesting 0))
(when (looking-at (concat (make-string (+ 6 (* 2 nesting)) ? )
"\\([A-Za-z:]+\\) \\(.*\\)"))
(let ((type (match-string 1))
(value (match-string 2)))
(pcase type
("Bool"
(if (equal value "true") 'true 'false))
((or "Float" "Id" "Int")
(string-to-number value))
("String"
(substring value 1 -1))
((or "Array:" "Struct:")
(let ((array '())
item)
(pw-cli--next-line)
(while (setq item (pw-cli--read-property (1+ nesting)))
(push item array)
(pw-cli--next-line))
(nreverse array)))))))
(defun pw-cli--parse-properties ()
(pw-cli--next-line)
(let ((end (or (save-excursion (re-search-forward "^ Object:" nil t))
(point-max)))
(properties '()))
(while (and (< (point) end)
(re-search-forward "^ Prop: key \\([A-Za-z:]+\\)" end t))
(pw-cli--next-line)
(let ((property (car (last (split-string (match-string 1) ":"))))
(value (pw-cli--read-property)))
(when value
(push (cons property value) properties))))
(goto-char end)
properties))
(cl-defmethod pw-access-properties ((_class pw-cli-accessor) node-id)
(with-temp-buffer
(pw-cli--command pw-cli-command `("enum-params" ,(number-to-string node-id) "Props"))
(pw-cli--parse-properties)))
(defun pw-cli--format-property-value (value)
(cond
((consp value)
(concat "[ " (mapconcat #'pw-cli--format-property-value value ", ") " ]"))
((numberp value)
(number-to-string value))
(t
value)))
(defun pw-cli--format-property (property)
(format "%s: %s" (car property) (pw-cli--format-property-value (cdr property))))
(defun pw-cli--format-properties (properties)
(concat "{ " (mapconcat #'pw-cli--format-property properties ", ") " }"))
(defun pw-cli--set-parameter (object-id parameter value)
(let* ((formatted (pw-cli--format-properties value)))
(call-process pw-cli-command nil pw-cli-command nil
"set-param" (number-to-string object-id) parameter formatted)))
(cl-defmethod pw-access-set-properties ((_class pw-cli-accessor) node-id properties)
(pw-cli--set-parameter node-id "Props" properties))
(cl-defmethod pw-access-current-profile ((_class pw-cli-accessor) device-id)
(with-temp-buffer
(pw-cli--command pw-cli-command `("enum-params" ,(number-to-string device-id) "Profile"))
(pw-cli--parse-properties)))
(cl-defmethod pw-access-profiles ((_class pw-cli-accessor) device-id)
(with-temp-buffer
(pw-cli--command pw-cli-command `("enum-params" ,(number-to-string device-id) "EnumProfile"))
(cl-loop for profile = (pw-cli--parse-properties) then (pw-cli--parse-properties)
while profile
collect profile)))
(cl-defmethod pw-access-set-profile ((_class pw-cli-accessor) device-id profile-index)
(pw-cli--set-parameter device-id "Profile" `(("index" . ,profile-index) ("save" . "true"))))
(defun pw-cli--parse-metadata ()
(let ((metadata '()))
(while (re-search-forward
"key:'\\([a-z.]+\\)'.*\\(value\\|\"name\"\\): ?['\"]\\([^'\"]+\\)['\"]"
nil t)
(push (cons (match-string 1) (match-string 3)) metadata))
metadata))
(cl-defmethod pw-access-defaults ((_class pw-cli-accessor))
(with-temp-buffer
(pw-cli--command pw-cli-metadata-command '("0"))
(pw-cli--parse-metadata)))
(cl-defmethod pw-access-set-default ((_class pw-cli-accessor) property node-name)
(call-process pw-cli-metadata-command nil pw-cli-metadata-command nil
"0" property (format "{ \"name\": \"%s\" }" node-name)))
(provide 'pw-access)
;;; pw-access.el ends here