Add an audio source picker to the GUI so users can capture a specific PipeWire device (sink monitor or microphone) instead of only the system default output. - vendor go2tv.app/screencast and patch the audio stream to accept a target PipeWire node serial (PW_KEY_TARGET_OBJECT); the upstream lib only ever auto-connected to the default - capture: ListAudioSources enumerates PipeWire sinks/sources via pw-dump; OpenPipeWire takes the selected node serial - flinger/config/gui: AudioSource config field, persisted and exposed as an Audio source dropdown (default output + enumerated devices) Also fixes two pre-existing bugs surfaced by stop/start testing: - engine Stop now waits for the video/audio/stats goroutines before destroying the encoder (was a use-after-free SIGSEGV) - Start/Stop now pause/resume the engine instead of tearing down and re-opening the portal session, which the portal cannot reliably do in-process (2nd CreateSession returned Ended/cancelled). Capture session stays open across stop/start.
65 lines
1.9 KiB
Go
65 lines
1.9 KiB
Go
package capture
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// TestPWParseAudioNodes parses a representative pw-dump payload into the
|
|
// node shape used by ListAudioSources.
|
|
func TestPWParseAudioNodes(t *testing.T) {
|
|
payload := `[
|
|
{"id": 51, "info": {"props": {"media.class": "Audio/Sink", "node.name": "alsa_out_speaker", "node.description": "Speaker", "object.serial": 1179}}},
|
|
{"id": 56, "info": {"props": {"media.class": "Audio/Source", "node.name": "alsa_in_mic", "node.description": "Stereo Mic", "object.serial": 1183}}},
|
|
{"id": 64, "info": {"props": {"media.class": "Audio/Device", "node.description": "Not capturable", "object.serial": 1172}}}
|
|
]`
|
|
|
|
var nodes []pwDumpNode
|
|
if err := json.Unmarshal([]byte(payload), &nodes); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
var devs []AudioDevice
|
|
for _, n := range nodes {
|
|
props := n.Info.Props
|
|
mc, _ := props["media.class"].(string)
|
|
if !audioNodeClass(mc) {
|
|
continue
|
|
}
|
|
devs = append(devs, AudioDevice{
|
|
Serial: toUint64(props["object.serial"]),
|
|
ID: n.ID,
|
|
Name: props["node.name"].(string),
|
|
Desc: props["node.description"].(string),
|
|
IsOutput: mc == "Audio/Sink",
|
|
})
|
|
}
|
|
|
|
if len(devs) != 2 {
|
|
t.Fatalf("got %d capturable devices, want 2", len(devs))
|
|
}
|
|
// Sink serial parsed and flagged as output.
|
|
if devs[0].Serial != 1179 || !devs[0].IsOutput {
|
|
t.Errorf("sink wrong: %+v", devs[0])
|
|
}
|
|
// Source serial parsed and flagged as input.
|
|
if devs[1].Serial != 1183 || devs[1].IsOutput {
|
|
t.Errorf("source wrong: %+v", devs[1])
|
|
}
|
|
}
|
|
|
|
// TestAudioNodeClass verifies which media classes are capturable.
|
|
func TestAudioNodeClass(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"Audio/Sink": true,
|
|
"Audio/Source": true,
|
|
"Audio/Device": false,
|
|
"Video/Source": false,
|
|
}
|
|
for cls, want := range cases {
|
|
if got := audioNodeClass(cls); got != want {
|
|
t.Errorf("audioNodeClass(%q) = %v, want %v", cls, got, want)
|
|
}
|
|
}
|
|
}
|