Skip to content

Commit 1dd4c88

Browse files
kasnderb0bbywan
authored andcommitted
fix Bluetooth client volume lookup
1 parent e41e334 commit 1dd4c88

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

backend/pulseaudio/pulseaudio.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,27 @@ func (pa *PulseAudioBackend) findSinkInput(name string) (pulseaudio.SinkInput, e
393393
return pulseaudio.SinkInput{}, fmt.Errorf("failed to list sink inputs: %w", err)
394394
}
395395
for _, s := range inputs {
396-
if strings.EqualFold(clientName(s.PropList), name) {
396+
if sinkInputMatchesName(s.PropList, name) {
397397
return s, nil
398398
}
399399
}
400400
return pulseaudio.SinkInput{}, &NotFoundError{Resource: "client", Name: name}
401401
}
402402

403+
// sinkInputMatchesName accepts both the raw PulseAudio stream name and the
404+
// Bluetooth display name exposed by parsePulseBluetoothSink. Bluetooth A2DP
405+
// inputs are implemented as module-loopback streams whose raw media.name is
406+
// "Loopback from <device>", while the API deliberately exposes just <device>.
407+
func sinkInputMatchesName(props map[string]string, name string) bool {
408+
if strings.EqualFold(clientName(props), name) {
409+
return true
410+
}
411+
if props["media.icon_name"] == "audio-card-bluetooth" {
412+
return strings.EqualFold(strings.TrimPrefix(props["media.name"], "Loopback from "), name)
413+
}
414+
return false
415+
}
416+
403417
func (pa *PulseAudioBackend) parseSinkInput(s pulseaudio.SinkInput) AudioClient {
404418
switch pa.kind {
405419
case ServerPipeWire:

backend/pulseaudio/pulseaudio_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,49 @@ func TestCloneProps(t *testing.T) {
127127
}
128128
}
129129

130+
func TestSinkInputMatchesName(t *testing.T) {
131+
tests := []struct {
132+
name string
133+
props map[string]string
134+
target string
135+
want bool
136+
}{
137+
{
138+
name: "regular stream name",
139+
props: map[string]string{
140+
"media.name": "Playback",
141+
},
142+
target: "Playback",
143+
want: true,
144+
},
145+
{
146+
name: "bluetooth display name",
147+
props: map[string]string{
148+
"media.name": "Loopback from Cloud Remaster",
149+
"media.icon_name": "audio-card-bluetooth",
150+
},
151+
target: "Cloud Remaster",
152+
want: true,
153+
},
154+
{
155+
name: "non-bluetooth prefix is not stripped",
156+
props: map[string]string{
157+
"media.name": "Loopback from Cloud Remaster",
158+
},
159+
target: "Cloud Remaster",
160+
want: false,
161+
},
162+
}
163+
164+
for _, tt := range tests {
165+
t.Run(tt.name, func(t *testing.T) {
166+
if got := sinkInputMatchesName(tt.props, tt.target); got != tt.want {
167+
t.Fatalf("sinkInputMatchesName() = %v, want %v", got, tt.want)
168+
}
169+
})
170+
}
171+
}
172+
130173
func TestExtractModuleSource(t *testing.T) {
131174
tests := []struct {
132175
name string

0 commit comments

Comments
 (0)