fix: recover from go-mp3 seek panic on startup (#86)
The go-mp3 library (v0.3.4) has a bug where Seek panics with a slice bounds error for certain byte positions. This crashes the app when restoring a saved playback position on startup. Add bounds clamping and a recover wrapper around the seek call in seekLocked to convert the panic into a graceful error. When the seek fails, playback starts from the beginning instead of crashing.
This commit is contained in:
@@ -100,6 +100,7 @@ var (
|
||||
errNoAudioFileLoaded = errors.New("no audio file loaded")
|
||||
errNoStreamerToPlay = errors.New("no streamer to play")
|
||||
errNoAudioStream = errors.New("no audio stream to pause")
|
||||
errSeekPanicked = errors.New("seek panicked (go-mp3 bug)")
|
||||
)
|
||||
|
||||
var speakerSampleRate = beep.SampleRate(44100)
|
||||
@@ -766,6 +767,19 @@ func (p *Player) seekLocked(targetSeconds int) error {
|
||||
),
|
||||
)
|
||||
|
||||
// Clamp the seek position to valid bounds. The underlying
|
||||
// go-mp3 library (v0.3.4) has a bug where seeking to
|
||||
// positions near the end of certain files causes a slice
|
||||
// bounds panic. Clamping reduces the likelihood of hitting
|
||||
// this, and the recover below catches it if it still occurs.
|
||||
if maxPos := p.seeker.Len() - 1; samples > maxPos {
|
||||
samples = maxPos
|
||||
}
|
||||
|
||||
if samples < 0 {
|
||||
samples = 0
|
||||
}
|
||||
|
||||
p.logger.Debug(
|
||||
"attempting to seek",
|
||||
"target-seconds", targetSeconds,
|
||||
@@ -773,9 +787,30 @@ func (p *Player) seekLocked(targetSeconds int) error {
|
||||
"samples", samples,
|
||||
)
|
||||
|
||||
if seekErr := p.seeker.Seek(samples); seekErr != nil {
|
||||
// Wrap the seek in a recover to catch panics from the
|
||||
// go-mp3 library's buggy Seek implementation. See:
|
||||
// github.com/hajimehoshi/go-mp3@v0.3.4/decode.go:111
|
||||
seekErr := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%w: %v", errSeekPanicked, r)
|
||||
}
|
||||
}()
|
||||
|
||||
return p.seeker.Seek(samples)
|
||||
}()
|
||||
|
||||
if seekErr != nil {
|
||||
speaker.Unlock()
|
||||
|
||||
p.logger.Warn(
|
||||
"Seek failed, playback will start from "+
|
||||
"the beginning",
|
||||
"target-seconds", targetSeconds,
|
||||
"samples", samples,
|
||||
"err", seekErr,
|
||||
)
|
||||
|
||||
return fmt.Errorf("failed to seek: %w", seekErr)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user