🎉 live server seems to be working now
This commit is contained in:
155
internal/pipewire/nodes.go
Normal file
155
internal/pipewire/nodes.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
ID int
|
||||
Name string
|
||||
Description string
|
||||
MediaClass string
|
||||
}
|
||||
|
||||
func (n Node) String() string {
|
||||
if n.Description != "" && n.Description != n.Name {
|
||||
return fmt.Sprintf("%s [%s]", n.Description, n.Name)
|
||||
}
|
||||
return n.Name
|
||||
}
|
||||
|
||||
type pwObject struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Info struct {
|
||||
Props struct {
|
||||
MediaClass string `json:"media.class"`
|
||||
NodeName string `json:"node.name"`
|
||||
NodeDesc string `json:"node.description"`
|
||||
} `json:"props"`
|
||||
} `json:"info"`
|
||||
}
|
||||
|
||||
func listAllNodes() ([]Node, error) {
|
||||
out, err := exec.Command("pw-dump").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pw-dump: %w", err)
|
||||
}
|
||||
|
||||
var objects []pwObject
|
||||
if err := json.Unmarshal(out, &objects); err != nil {
|
||||
return nil, fmt.Errorf("parse pw-dump: %w", err)
|
||||
}
|
||||
|
||||
var nodes []Node
|
||||
for _, o := range objects {
|
||||
if o.Type != "PipeWire:Interface:Node" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(o.Info.Props.MediaClass, "Audio/") || o.Info.Props.NodeName == "" {
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, Node{
|
||||
ID: o.ID,
|
||||
Name: o.Info.Props.NodeName,
|
||||
Description: o.Info.Props.NodeDesc,
|
||||
MediaClass: o.Info.Props.MediaClass,
|
||||
})
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func ListSinks() ([]Node, error) {
|
||||
all, err := listAllNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Node
|
||||
for _, n := range all {
|
||||
if n.MediaClass == "Audio/Sink" {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListSources() ([]Node, error) {
|
||||
all, err := listAllNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Node
|
||||
for _, n := range all {
|
||||
switch n.MediaClass {
|
||||
case "Audio/Source", "Audio/Source.Virtual":
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CaptureTarget is a node that can be fed to `pw-cat --capture --target`.
|
||||
// Real microphones / line-ins / virtual sources have IsMonitor=false.
|
||||
// Audio sinks are surfaced with IsMonitor=true: pw-cat reads the monitor
|
||||
// port when given a sink as --target, so the live server can stream
|
||||
// whatever audio the host is currently playing.
|
||||
type CaptureTarget struct {
|
||||
Name string
|
||||
Description string
|
||||
IsMonitor bool
|
||||
}
|
||||
|
||||
// ListCaptureTargets returns every PipeWire node usable as audio input
|
||||
// for the live server. Sinks are surfaced as monitor capture targets
|
||||
// (pw-cat captures the monitor port when given a sink as --target);
|
||||
// `.monitor` pseudo-sources exposed by the PulseAudio compatibility
|
||||
// layer are also included since `pw-cat --target <name>.monitor` is
|
||||
// the canonical way to capture playback on many setups.
|
||||
//
|
||||
// Order: real microphones / line-ins first, then monitors (the things
|
||||
// most users actually want for "stream what's playing").
|
||||
func ListCaptureTargets() ([]CaptureTarget, error) {
|
||||
all, err := listAllNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var realSources, monitorSources, sinkMonitors []CaptureTarget
|
||||
monitorNames := map[string]bool{}
|
||||
|
||||
for _, n := range all {
|
||||
switch n.MediaClass {
|
||||
case "Audio/Source", "Audio/Source.Virtual":
|
||||
if strings.HasSuffix(n.Name, ".monitor") {
|
||||
monitorSources = append(monitorSources, CaptureTarget{
|
||||
Name: n.Name, Description: n.Description, IsMonitor: true,
|
||||
})
|
||||
monitorNames[n.Name] = true
|
||||
} else {
|
||||
realSources = append(realSources, CaptureTarget{
|
||||
Name: n.Name, Description: n.Description,
|
||||
})
|
||||
}
|
||||
case "Audio/Sink":
|
||||
// Skip the sink itself when its corresponding ".monitor"
|
||||
// source already covers the same capture path — pick one
|
||||
// canonical name (the source) to avoid two indistinguishable
|
||||
// dropdown entries.
|
||||
if monitorNames[n.Name+".monitor"] {
|
||||
continue
|
||||
}
|
||||
sinkMonitors = append(sinkMonitors, CaptureTarget{
|
||||
Name: n.Name, Description: n.Description, IsMonitor: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]CaptureTarget, 0, len(realSources)+len(monitorSources)+len(sinkMonitors))
|
||||
out = append(out, realSources...)
|
||||
out = append(out, monitorSources...)
|
||||
out = append(out, sinkMonitors...)
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user