Skip to content

Commit ded0f0d

Browse files
committed
fix: keep mpv IPC responsive under event bursts
1 parent 6ff0f7f commit ded0f0d

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ curl -fsSL https://raw.githubusercontent.com/iRootPro/lofi-player/main/scripts/i
7575

7676
Auto-detects OS/arch, pulls the matching tarball from the latest
7777
release, drops the binary into `~/.local/bin`. Override with
78-
`INSTALL_DIR=/usr/local/bin` or pin to a tag with `VERSION=v0.1.2`.
78+
`INSTALL_DIR=/usr/local/bin` or pin to a tag with `VERSION=v0.1.3`.
7979

8080
You'll also need `mpv` (and optionally `yt-dlp`) — see
8181
[runtime dependencies](#runtime-dependencies) below. The installer

README.ru.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ curl -fsSL https://raw.githubusercontent.com/iRootPro/lofi-player/main/scripts/i
7979

8080
Сам определяет OS/arch, скачивает архив из последнего релиза, кладёт
8181
бинарник в `~/.local/bin`. Переопределить можно через
82-
`INSTALL_DIR=/usr/local/bin`, заПинить версию — через `VERSION=v0.1.2`.
82+
`INSTALL_DIR=/usr/local/bin`, заПинить версию — через `VERSION=v0.1.3`.
8383

8484
`mpv` (и опционально `yt-dlp`) поставь отдельно — см.
8585
[системные зависимости](#системные-зависимости) ниже. Инсталлер

internal/audio/mpv.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,19 @@ func (c *ipcClient) readLoop(maxBuf int) {
180180
Data: msg.Data,
181181
Reason: msg.Reason,
182182
}
183+
// Do not let event backpressure starve command responses. Some
184+
// streams (notably YouTube via ytdl_hook) can emit bursts of
185+
// property changes while loading or buffering; if the TUI is not
186+
// draining events fast enough, blocking here prevents this read loop
187+
// from reaching later request_id responses and harmless commands like
188+
// set volume time out with context deadline exceeded. Dropping an
189+
// over-capacity event is preferable: observed properties are state
190+
// snapshots and mpv will send another update soon.
183191
select {
184192
case c.events <- evt:
185193
case <-c.done:
186194
return
195+
default:
187196
}
188197
case msg.RequestID != 0:
189198
if ch, ok := c.pending.LoadAndDelete(msg.RequestID); ok {

internal/audio/mpv_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,69 @@ func TestReadLoop_MalformedLineSkipped(t *testing.T) {
182182
}
183183
}
184184

185+
func TestCommand_ResponseNotStarvedByFullEventQueue(t *testing.T) {
186+
c, server := pipePair(t, defaultIPCBuf)
187+
188+
result := make(chan error, 1)
189+
go func() {
190+
_, err := c.command(context.Background(), "set_property", "volume", 55)
191+
result <- err
192+
}()
193+
194+
scanner := bufio.NewScanner(server)
195+
if !scanner.Scan() {
196+
t.Fatal("scanner ended before reading request")
197+
}
198+
var req ipcRequest
199+
if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
200+
t.Fatalf("decode request: %v", err)
201+
}
202+
203+
// Simulate a noisy stream that fills the event queue before mpv's
204+
// command response arrives. The IPC reader must keep reading and route
205+
// the response instead of blocking forever on an undrained Events chan.
206+
writeErr := make(chan error, 1)
207+
go func() {
208+
for i := 0; i < cap(c.events)+10; i++ {
209+
line := fmt.Sprintf(`{"event":"property-change","id":9,"name":"demuxer-cache-state","data":{"fw-bytes":%d}}`+"\n", i)
210+
if _, err := server.Write([]byte(line)); err != nil {
211+
writeErr <- fmt.Errorf("write event %d: %w", i, err)
212+
return
213+
}
214+
}
215+
resp, _ := json.Marshal(ipcResponse{RequestID: req.RequestID, Error: "success"})
216+
if _, err := server.Write(append(resp, '\n')); err != nil {
217+
writeErr <- fmt.Errorf("write response: %w", err)
218+
return
219+
}
220+
writeErr <- nil
221+
}()
222+
223+
select {
224+
case err := <-result:
225+
if err != nil {
226+
t.Fatalf("command returned error: %v", err)
227+
}
228+
if err := <-writeErr; err != nil {
229+
t.Fatal(err)
230+
}
231+
case err := <-writeErr:
232+
if err != nil {
233+
t.Fatal(err)
234+
}
235+
select {
236+
case err := <-result:
237+
if err != nil {
238+
t.Fatalf("command returned error: %v", err)
239+
}
240+
case <-time.After(time.Second):
241+
t.Fatal("timeout waiting for command response after writes completed")
242+
}
243+
case <-time.After(time.Second):
244+
t.Fatal("timeout waiting for command response; event backpressure likely starved responses")
245+
}
246+
}
247+
185248
func TestCommand_ContextCancellation(t *testing.T) {
186249
c, _ := pipePair(t, defaultIPCBuf)
187250

0 commit comments

Comments
 (0)