Skip to content

Commit

Permalink
Add PipeWire support
Browse files Browse the repository at this point in the history
  • Loading branch information
diamondburned committed Dec 25, 2022
1 parent 860b580 commit 81e4318
Show file tree
Hide file tree
Showing 4 changed files with 535 additions and 0 deletions.
1 change: 1 addition & 0 deletions input/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ package all
import (
_ "github.com/noriah/catnip/input/ffmpeg"
_ "github.com/noriah/catnip/input/parec"
_ "github.com/noriah/catnip/input/pipewire"
)
140 changes: 140 additions & 0 deletions input/pipewire/dump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package pipewire

import (
"context"
"encoding/json"
"os"
"os/exec"

"github.com/pkg/errors"
)

type pwObjects []pwObject

func pwDump(ctx context.Context) (pwObjects, error) {
cmd := exec.CommandContext(ctx, "pw-dump")
cmd.Stderr = os.Stderr

dumpOutput, err := cmd.Output()
if err != nil {
var execErr *exec.ExitError
if errors.As(err, &execErr) {
return nil, errors.Wrapf(err, "failed to run pw-dump: %s", execErr.Stderr)
}
return nil, errors.Wrap(err, "failed to run pw-dump")
}

var dump pwObjects
if err := json.Unmarshal(dumpOutput, &dump); err != nil {
return nil, errors.Wrap(err, "failed to parse pw-dump output")
}

return dump, nil
}

// Filter filters for the devices that satisfies f.
func (d pwObjects) Filter(fns ...func(pwObject) bool) pwObjects {
filtered := make(pwObjects, 0, len(d))
loop:
for _, device := range d {
for _, f := range fns {
if !f(device) {
continue loop
}
}
filtered = append(filtered, device)
}
return filtered
}

// Find returns the first object that satisfies f.
func (d pwObjects) Find(f func(pwObject) bool) *pwObject {
for i, device := range d {
if f(device) {
return &d[i]
}
}
return nil
}

// ResolvePorts returns all PipeWire port objects that belong to the given
// object.
func (d pwObjects) ResolvePorts(object *pwObject, dir pwPortDirection) pwObjects {
return d.Filter(
func(o pwObject) bool { return o.Type == pwInterfacePort },
func(o pwObject) bool {
return o.Info.Props.NodeID == object.ID && o.Info.Props.PortDirection == dir
},
)
}

type pwObjectID int64

type pwObjectType string

const (
pwInterfaceDevice pwObjectType = "PipeWire:Interface:Device"
pwInterfaceNode pwObjectType = "PipeWire:Interface:Node"
pwInterfacePort pwObjectType = "PipeWire:Interface:Port"
pwInterfaceLink pwObjectType = "PipeWire:Interface:Link"
)

type pwObject struct {
ID pwObjectID `json:"id"`
Type pwObjectType `json:"type"`
Info struct {
Props pwInfoProps `json:"props"`
} `json:"info"`
}

type pwInfoProps struct {
pwDeviceProps
pwNodeProps
pwPortProps
MediaClass string `json:"media.class"`

JSON json.RawMessage `json:"-"`
}

func (p *pwInfoProps) UnmarshalJSON(data []byte) error {
type Alias pwInfoProps
if err := json.Unmarshal(data, (*Alias)(p)); err != nil {
return err
}
p.JSON = append([]byte(nil), data...)
return nil
}

type pwDeviceProps struct {
DeviceName string `json:"device.name"`
}

// pwNodeProps is for Audio/Sink only.
type pwNodeProps struct {
NodeName string `json:"node.name"`
NodeNick string `json:"node.nick"`
NodeDescription string `json:"node.description"`
}

// Constants for MediaClass.
const (
pwAudioDevice string = "Audio/Device"
pwAudioSink string = "Audio/Sink"
pwStreamOutputAudio string = "Stream/Output/Audio"
)

type pwPortDirection string

const (
pwPortIn = "in"
pwPortOut = "out"
)

type pwPortProps struct {
PortID pwObjectID `json:"port.id"`
PortName string `json:"port.name"`
PortAlias string `json:"port.alias"`
PortDirection pwPortDirection `json:"port.direction"`
NodeID pwObjectID `json:"node.id"`
ObjectPath string `json:"object.path"`
}
127 changes: 127 additions & 0 deletions input/pipewire/link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package pipewire

import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"

"github.com/pkg/errors"
)

func pwLink(outPortID, inPortID pwObjectID) error {
cmd := exec.Command("pw-link", "-L", fmt.Sprint(outPortID), fmt.Sprint(inPortID))
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.Stderr != nil {
return errors.Wrapf(err, "failed to run pw-link: %s", exitErr.Stderr)
}
return err
}
return nil
}

type pwLinkObject struct {
ID pwObjectID
DeviceName string
PortName string // usually like {input,output}_{FL,FR}
}

func pwLinkObjectParse(line string) (pwLinkObject, error) {
var obj pwLinkObject

idStr, portStr, ok := strings.Cut(line, " ")
if !ok {
return obj, fmt.Errorf("failed to parse pw-link object %q", line)
}

id, err := strconv.Atoi(idStr)
if err != nil {
return obj, errors.Wrapf(err, "failed to parse pw-link object id %q", idStr)
}

name, port, ok := strings.Cut(portStr, ":")
if !ok {
return obj, fmt.Errorf("failed to parse pw-link port string %q", portStr)
}

obj = pwLinkObject{
ID: pwObjectID(id),
DeviceName: name,
PortName: port,
}

return obj, nil
}

type pwLinkType string

const (
pwLinkInputPorts pwLinkType = "i"
pwLinkOutputPorts pwLinkType = "o"
)

type pwLinkEvent interface {
pwLinkEvent()
}

type pwLinkAdd pwLinkObject
type pwLinkRemove pwLinkObject

func (pwLinkAdd) pwLinkEvent() {}
func (pwLinkRemove) pwLinkEvent() {}

func pwLinkMonitor(ctx context.Context, typ pwLinkType, ch chan<- pwLinkEvent) error {
cmd := exec.CommandContext(ctx, "pw-link", "-mI"+string(typ))
cmd.Stderr = os.Stderr

o, err := cmd.StdoutPipe()
if err != nil {
return errors.Wrap(err, "failed to get stdout pipe")
}
defer o.Close()

if err := cmd.Start(); err != nil {
return errors.Wrap(err, "pw-link -m")
}

scanner := bufio.NewScanner(o)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}

mark := line[0]

line = strings.TrimSpace(line[1:])

obj, err := pwLinkObjectParse(line)
if err != nil {
continue
}

var ev pwLinkEvent
switch mark {
case '=':
fallthrough
case '+':
ev = pwLinkAdd(obj)
case '-':
ev = pwLinkRemove(obj)
default:
continue
}

select {
case <-ctx.Done():
return ctx.Err()
case ch <- ev:
}
}

return errors.Wrap(cmd.Wait(), "pw-link exited")
}
Loading

0 comments on commit 81e4318

Please sign in to comment.