You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: pkg/music/README.md
+118-3Lines changed: 118 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,40 @@
2
2
3
3
Queue-based music playback library for Go with pluggable audio sinks and track resolvers. Resolves URLs and search queries (YouTube, SoundCloud, radio), opens PCM streams via multiple parsers (yt-dlp, kkdai, ffmpeg), and plays through a sink of your choice (e.g. speaker or custom Discord voice).
4
4
5
+
## How it works (high level)
6
+
7
+
At runtime the system is a pipeline:
8
+
9
+
-**Resolve** user input → `sources.TrackInfo` (URL, title, available parsers)
10
+
-**Enqueue** resolved tracks into a FIFO queue
11
+
-**Open stream** using one of the available parsers (PCM `s16le` @ 48kHz stereo)
12
+
-**Stream to sink** (speaker / Discord / custom) until the track ends or fails
13
+
-**Recover** when possible (parser fallback on instant-open failures; reopen on early EOF; special handling for voice transport)
_ = p.PlayNext("") // "" for local; use voice channel ID for Discord
25
59
```
26
60
27
-
Listen to `p.PlayerStatus` for status updates (Playing, Added, Stopped, Error). See [examples/cli_speaker](examples/cli_speaker) for a full runnable CLI.
61
+
Listen to `p.PlayerStatus` for status updates (Playing, Added, Stopped, Error). See [examples/clispeaker](examples/clispeaker) for a full runnable CLI.
62
+
63
+
## Algorithms (by stage)
64
+
65
+
### 1) Resolve (input → TrackInfo)
66
+
67
+
Goal: convert user input into canonical metadata + a parser preference list.
68
+
69
+
-**Input**: URL or search query + optional `source`/`parser` hints.
70
+
-**Output**: `[]sources.TrackInfo` where `TrackInfo.AvailableParsers` is ordered by preference.
71
+
72
+
The resolver is intentionally pluggable; the player does not care *how* a track was discovered, only that it has a URL + parsers list.
73
+
74
+
### 2) Enqueue (TrackInfo → queue)
75
+
76
+
Goal: turn `TrackInfo` into `parsers.TrackParse` and append to the FIFO queue.
77
+
78
+
- Tracks without `AvailableParsers` are rejected/skipped.
79
+
-`CurrentParser` starts as the first entry in `AvailableParsers` (will be updated later by recovery/open logic).
80
+
81
+
### 3) Start playback (dequeue → open resilient stream)
82
+
83
+
Performed by `Player.PlayNext()`:
84
+
85
+
- If something is playing, stop it.
86
+
- Pop the next track from the queue.
87
+
- Create `stream.RecoveryStream(track)` and call `rs.Open(seek=0)`.
88
+
- If open fails for all parsers, skip the track and try the next.
89
+
90
+
### 4) Open stream (choose parser)
91
+
92
+
Performed inside `RecoveryStream.Open(seek)`:
93
+
94
+
- Starting at `parserIndex`, iterate through `track.SourceInfo.AvailableParsers`.
95
+
- For each parser:
96
+
- if `retries[parser] >= maxRecoveryAttempts` → skip
97
+
- try `openWithParser(track, parser, seek)`
98
+
- on success:
99
+
- set `parserIndex` to that parser’s index
100
+
- set `track.CurrentParser = parser`
101
+
- reset `firstRead = true`
102
+
- store cleanup + current seek
103
+
104
+
### 5) Media recovery (parser/ffmpeg level)
105
+
106
+
Recovery is intentionally conservative to avoid false-positive “fallback” when a track naturally ends.
107
+
108
+
**A) Instant failure right after open**
109
+
110
+
If the very first `Read()` on the opened stream returns any error (including an EOF-like failure from ffmpeg), it is treated as an *instant fail*:
111
+
112
+
- close/cleanup current stream
113
+
-`parserIndex++`
114
+
- open again at the current `seekSec` using the next parser
115
+
116
+
This is designed for cases like “ffmpeg opened, then immediately 403/forbidden and closed stdout”.
117
+
118
+
**B) Early EOF (mid-track)**
119
+
120
+
If `Read()` returns `io.EOF` with `n==0` and the track is far from its expected duration, recovery attempts to reopen:
121
+
122
+
- close/cleanup current stream
123
+
- reopen at the current approximate `seekSec`
124
+
- retries are bounded by `maxRecoveryAttempts` per parser
125
+
126
+
If duration is unknown, early-EOF recovery is only attempted at the beginning (`firstRead` or `seekSec < 1.0`).
127
+
128
+
### 6) Sink streaming + voice transport recovery
129
+
130
+
The sink drives the read loop via `AudioSink.Stream(reader, stopCh)`:
131
+
132
+
- On normal completion: the track ends → player advances to the next track.
133
+
- On `stream.ErrVoiceTransport` (Discord transport issues):
134
+
- the player can invalidate/rejoin the sink (hard) or retry without rejoin (soft mode)
135
+
- then calls `rs.ReopenAfterTransportFailure()` to reopen media at the current seek
136
+
- On user stop/skip: playback stops cleanly.
137
+
138
+
## Key extension points
139
+
140
+
-**Custom resolver**: implement `player.Resolver` to support new sources or search.
141
+
-**Custom sink**: implement `sink.AudioSink` / `sink.Provider` to support new outputs.
142
+
-**New parser**: implement `parsers.Streamer` and register it in `stream.Registry`.
28
143
29
144
## Requirements
30
145
@@ -35,7 +150,7 @@ Listen to `p.PlayerStatus` for status updates (Playing, Added, Stopped, Error).
35
150
## Documentation
36
151
37
152
-[player](player) — Queue-based playback engine
38
-
-[resolver](resolver) — Resolve URLs and search to track metadata
153
+
-[resolve](resolve) — Resolve URLs and search to track metadata
39
154
-[sink](sink) — Audio sink interfaces and speaker implementation
40
155
-[sources](sources) — Source interface and track types
41
156
-[parsers](parsers) — Streamer interface and track type
0 commit comments