-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
860b580
commit 81e4318
Showing
4 changed files
with
535 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.