Skip to content

Commit

Permalink
Add PipeWire support
Browse files Browse the repository at this point in the history
  • Loading branch information
diamondburned committed Oct 8, 2022
1 parent b90b86d commit 2abd9e7
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 0 deletions.
1 change: 1 addition & 0 deletions catnip.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

_ "github.com/noriah/catnip/input/ffmpeg"
_ "github.com/noriah/catnip/input/parec"
_ "github.com/noriah/catnip/input/pipewire"

"github.com/integrii/flaggy"
"github.com/pkg/errors"
Expand Down
20 changes: 20 additions & 0 deletions input/pipewire/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package pipewire

import (
"fmt"
"os/exec"

"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) {
return errors.Wrapf(err, "failed to run pw-link: %s", exitErr.Stderr)
}
return err
}
return nil
}
188 changes: 188 additions & 0 deletions input/pipewire/dump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package pipewire

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

"github.com/pkg/errors"
)

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 pwDump []pwObject

func pwObjects() (pwDump, error) {
dumpOutput, err := exec.Command("pw-dump").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 pwDump
if err := json.Unmarshal(dumpOutput, &dump); err != nil {
return nil, errors.Wrap(err, "failed to parse pw-dump output")
}

return dump, nil
}

func (d pwDump) Links() pwDump {
return d.
Filter(func(o pwObject) bool { return o.Type == pwInterfaceLink })
}

func (d pwDump) AudioSinks() pwDump {
return d.Filter(
func(o pwObject) bool { return o.Type == pwInterfaceNode },
func(o pwObject) bool { return o.Info.Props.MediaClass == pwAudioSink },
)
}

// Filter filters for the devices that satisfies f.
func (d pwDump) Filter(fns ...func(pwObject) bool) pwDump {
filtered := make(pwDump, 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 pwDump) Find(f func(pwObject) bool) *pwObject {
for i, device := range d {
if f(device) {
return &d[i]
}
}
return nil
}

// Object gets the object with the given ID.
func (d pwDump) Object(id pwObjectID) *pwObject {
for i, o := range d {
if o.ID == id {
return &d[i]
}
}
return nil
}

// TODO: generate a unique ID for catnip, so we can easier look up this stuff.

type pwPortDirection string

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

// ResolvePorts returns all PipeWire port objects that belong to the given
// object.
func (d pwDump) ResolvePorts(object *pwObject, dir pwPortDirection) pwDump {
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
},
)
}

// OutputLinks returns all links that are connected to the given object.
func (d pwDump) OutputLinks(output *pwObject) pwDump {
return d.Filter(
func(o pwObject) bool { return o.Type == pwInterfaceLink },
func(o pwObject) bool { return o.Info.Props.LinkOutputNode == output.ID },
)
}

// InputLinks returns all links that are connected to the given object.
func (d pwDump) InputLinks(output *pwObject) pwDump {
return d.Filter(
func(o pwObject) bool { return o.Type == pwInterfaceLink },
func(o pwObject) bool { return o.Info.Props.LinkInputNode == output.ID },
)
}

type pwObjectID int64

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

type pwInfoProps struct {
pwDeviceProps
pwNodeProps
pwLinkProps
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"
)

// pwLinkProps is for links only. NodeID is the object ID; PortID is the port
// object's ID. Input would be our catnip. Output would be the sink.
//
// We don't actually need to resolve the PortID to remove it, but we do need to
// find all relevant ports to create new links. We can do this by filtering for
// all Ports with the same NodeID.
type pwLinkProps struct {
LinkOutputNode pwObjectID `json:"link.output.node"`
LinkOutputPort pwObjectID `json:"link.output.port"`
LinkInputNode pwObjectID `json:"link.input.node"`
LinkInputPort pwObjectID `json:"link.input.port"`
}

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"`
}

0 comments on commit 2abd9e7

Please sign in to comment.