diff --git a/pipewire-access.el b/pipewire-access.el index be91571..75e0fc4 100644 --- a/pipewire-access.el +++ b/pipewire-access.el @@ -1,4 +1,4 @@ -;;; pipewire-access.el --- PipeWire access -*- lexical-binding: t -*- +;;; pipewire-access.el --- PipeWire generic access -*- lexical-binding: t -*- ;; Copyright (C) 2022 Milan Zamazal @@ -28,14 +28,6 @@ ;; 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. ;;; Code: @@ -131,146 +123,6 @@ as reported in `pipewire-access-defaults' and NODE-NAME is a string name of the node that should be assigned to KEY. CLASS is a PipeWire interface, see symbol `pipewire-accessor'.") -;; pw-cli interface - -(defvar pipewire-cli-command "pw-cli" - "Command to invoke pw-cli.") - -(defvar pipewire-cli-metadata-command "pw-metadata" - "Command to invoke pw-metadata.") - -(defclass pipewire-cli-accessor (pipewire-accessor) - () - :documentation - "Command line based interface to PipeWire. -Note this interface may not work with all PipeWire versions.") - -(defun pipewire-cli--command (command args) - (apply #'call-process command nil t nil args) - (goto-char (point-min))) - -(defun pipewire-cli--next-line () - (goto-char (line-beginning-position 2))) - -(defun pipewire-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))))) - (pipewire-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)) - (pipewire-cli--next-line)) - (push (cons id properties) objects))) - (nreverse objects))) - -(cl-defmethod pipewire-access-objects ((_class pipewire-cli-accessor)) - (with-temp-buffer - (pipewire-cli--command pipewire-cli-command '("list-objects")) - (pipewire-cli--parse-list))) - -(defun pipewire-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) - (pipewire-cli--next-line) - (while (setq item (pipewire-cli--read-property (1+ nesting))) - (push item array) - (pipewire-cli--next-line)) - (nreverse array))))))) - -(defun pipewire-cli--parse-properties () - (pipewire-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)) - (pipewire-cli--next-line) - (let ((property (car (last (split-string (match-string 1) ":")))) - (value (pipewire-cli--read-property))) - (when value - (push (cons property value) properties)))) - (goto-char end) - properties)) - -(cl-defmethod pipewire-access-properties ((_class pipewire-cli-accessor) node-id) - (with-temp-buffer - (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string node-id) "Props")) - (pipewire-cli--parse-properties))) - -(defun pipewire-cli--format-property-value (value) - (cond - ((consp value) - (concat "[ " (mapconcat #'pipewire-cli--format-property-value value ", ") " ]")) - ((numberp value) - (number-to-string value)) - (t - value))) - -(defun pipewire-cli--format-property (property) - (format "%s: %s" (car property) (pipewire-cli--format-property-value (cdr property)))) - -(defun pipewire-cli--format-properties (properties) - (concat "{ " (mapconcat #'pipewire-cli--format-property properties ", ") " }")) - -(defun pipewire-cli--set-parameter (object-id parameter value) - (let* ((formatted (pipewire-cli--format-properties value))) - (call-process pipewire-cli-command nil pipewire-cli-command nil - "set-param" (number-to-string object-id) parameter formatted))) - -(cl-defmethod pipewire-access-set-properties ((_class pipewire-cli-accessor) node-id properties) - (pipewire-cli--set-parameter node-id "Props" properties)) - -(cl-defmethod pipewire-access-current-profile ((_class pipewire-cli-accessor) device-id) - (with-temp-buffer - (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "Profile")) - (pipewire-cli--parse-properties))) - -(cl-defmethod pipewire-access-profiles ((_class pipewire-cli-accessor) device-id) - (with-temp-buffer - (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "EnumProfile")) - (cl-loop for profile = (pipewire-cli--parse-properties) then (pipewire-cli--parse-properties) - while profile - collect profile))) - -(cl-defmethod pipewire-access-set-profile ((_class pipewire-cli-accessor) device-id profile-index) - (pipewire-cli--set-parameter device-id "Profile" `(("index" . ,profile-index) ("save" . "true")))) - -(defun pipewire-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 pipewire-access-defaults ((_class pipewire-cli-accessor)) - (with-temp-buffer - (pipewire-cli--command pipewire-cli-metadata-command '("0")) - (pipewire-cli--parse-metadata))) - -(cl-defmethod pipewire-access-set-default ((_class pipewire-cli-accessor) property node-name) - (call-process pipewire-cli-metadata-command nil pipewire-cli-metadata-command nil - "0" property (format "{ \"name\": \"%s\" }" node-name))) - (provide 'pipewire-access) ;; Local Variables: diff --git a/pipewire-cli.el b/pipewire-cli.el new file mode 100644 index 0000000..c1d7930 --- /dev/null +++ b/pipewire-cli.el @@ -0,0 +1,185 @@ +;;; pipewire-cli.el --- PipeWire command line access -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Milan Zamazal + +;; Author: Milan Zamazal +;; 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 . + +;;; Commentary: +;; +;; 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. + +;;; Code: + +(require 'eieio) +(require 'pipewire-access) + +(defvar pipewire-cli-command "pw-cli" + "Command to invoke pw-cli.") + +(defvar pipewire-cli-metadata-command "pw-metadata" + "Command to invoke pw-metadata.") + +(defclass pipewire-cli-accessor (pipewire-accessor) + () + :documentation + "Command line based interface to PipeWire. +Note this interface may not work with all PipeWire versions.") + +(defun pipewire-cli--command (command args) + (apply #'call-process command nil t nil args) + (goto-char (point-min))) + +(defun pipewire-cli--next-line () + (goto-char (line-beginning-position 2))) + +(defun pipewire-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))))) + (pipewire-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)) + (pipewire-cli--next-line)) + (push (cons id properties) objects))) + (nreverse objects))) + +(cl-defmethod pipewire-access-objects ((_class pipewire-cli-accessor)) + (with-temp-buffer + (pipewire-cli--command pipewire-cli-command '("list-objects")) + (pipewire-cli--parse-list))) + +(defun pipewire-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) + (pipewire-cli--next-line) + (while (setq item (pipewire-cli--read-property (1+ nesting))) + (push item array) + (pipewire-cli--next-line)) + (nreverse array))))))) + +(defun pipewire-cli--parse-properties () + (pipewire-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)) + (pipewire-cli--next-line) + (let ((property (car (last (split-string (match-string 1) ":")))) + (value (pipewire-cli--read-property))) + (when value + (push (cons property value) properties)))) + (goto-char end) + properties)) + +(cl-defmethod pipewire-access-properties ((_class pipewire-cli-accessor) node-id) + (with-temp-buffer + (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string node-id) "Props")) + (pipewire-cli--parse-properties))) + +(defun pipewire-cli--format-property-value (value) + (cond + ((consp value) + (concat "[ " (mapconcat #'pipewire-cli--format-property-value value ", ") " ]")) + ((numberp value) + (number-to-string value)) + (t + value))) + +(defun pipewire-cli--format-property (property) + (format "%s: %s" (car property) (pipewire-cli--format-property-value (cdr property)))) + +(defun pipewire-cli--format-properties (properties) + (concat "{ " (mapconcat #'pipewire-cli--format-property properties ", ") " }")) + +(defun pipewire-cli--set-parameter (object-id parameter value) + (let* ((formatted (pipewire-cli--format-properties value))) + (call-process pipewire-cli-command nil pipewire-cli-command nil + "set-param" (number-to-string object-id) parameter formatted))) + +(cl-defmethod pipewire-access-set-properties ((_class pipewire-cli-accessor) node-id properties) + (pipewire-cli--set-parameter node-id "Props" properties)) + +(cl-defmethod pipewire-access-current-profile ((_class pipewire-cli-accessor) device-id) + (with-temp-buffer + (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "Profile")) + (pipewire-cli--parse-properties))) + +(cl-defmethod pipewire-access-profiles ((_class pipewire-cli-accessor) device-id) + (with-temp-buffer + (pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "EnumProfile")) + (cl-loop for profile = (pipewire-cli--parse-properties) then (pipewire-cli--parse-properties) + while profile + collect profile))) + +(cl-defmethod pipewire-access-set-profile ((_class pipewire-cli-accessor) device-id profile-index) + (pipewire-cli--set-parameter device-id "Profile" `(("index" . ,profile-index) ("save" . "true")))) + +(defun pipewire-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 pipewire-access-defaults ((_class pipewire-cli-accessor)) + (with-temp-buffer + (pipewire-cli--command pipewire-cli-metadata-command '("0")) + (pipewire-cli--parse-metadata))) + +(cl-defmethod pipewire-access-set-default ((_class pipewire-cli-accessor) property node-name) + (call-process pipewire-cli-metadata-command nil pipewire-cli-metadata-command nil + "0" property (format "{ \"name\": \"%s\" }" node-name))) + +(provide 'pipewire-cli) + +;; Local Variables: +;; checkdoc-force-docstrings-flag: nil +;; End: + +;;; pipewire-cli.el ends here diff --git a/pipewire-lib.el b/pipewire-lib.el index e99dca2..82a7fca 100644 --- a/pipewire-lib.el +++ b/pipewire-lib.el @@ -36,6 +36,7 @@ (require 'cl-lib) (require 'pipewire-access) +(require 'pipewire-cli) (defvar pipewire-lib--accessor (pipewire-cli-accessor))