Add playback stall watchdog and recovery

This commit is contained in:
Stu Leak 2026-01-11 05:44:21 -05:00
parent 84fd0a286b
commit 6a57a3ccdc

118
main.go
View File

@ -1167,13 +1167,18 @@ type appState struct {
authorCreateMenu bool // Whether to create DVD menu
authorMenuTemplate string // "Simple", "Dark", "Poster"
authorMenuBackgroundImage string // Path to a user-selected background image
authorMenuTheme string // "VideoTools"
authorMenuLogoEnabled bool
authorMenuLogoPath string // Path to menu logo image
authorMenuLogoPosition string // "Top Left", "Top Right", "Bottom Left", "Bottom Right", "Center"
authorMenuLogoScale float64
authorMenuLogoMargin int
authorMenuStructure string // Feature only, Chapters, Extras
authorMenuTheme string // "VideoTools"
authorMenuTitleLogoEnabled bool // Enable title logo (main logo above menu)
authorMenuTitleLogoPath string // Path to title logo image
authorMenuTitleLogoPosition string // Position for title logo
authorMenuTitleLogoScale float64 // Scale for title logo
authorMenuTitleLogoMargin int // Margin for title logo
authorMenuStudioLogoEnabled bool // Enable studio logo (corner logo)
authorMenuStudioLogoPath string // Path to studio logo image
authorMenuStudioLogoPosition string // "Top Left", "Top Right", "Bottom Left", "Bottom Right"
authorMenuStudioLogoScale float64 // Scale for studio logo
authorMenuStudioLogoMargin int // Margin for studio logo
authorMenuStructure string // Feature only, Chapters, Extras
authorMenuExtrasEnabled bool // Show extras menu
authorMenuChapterThumbSrc string // Auto, First Frame, Midpoint, Custom
authorTitle string // DVD title
@ -11228,6 +11233,8 @@ type playSession struct {
videoTime float64 // Last video frame time
syncOffset float64 // A/V sync offset for adjustment
audioActive atomic.Bool // Whether audio stream is running
stallCount int
lastFrameAt time.Time
// GStreamer player for stable A/V playback
gstPlayer *player.GStreamerPlayer
@ -11493,6 +11500,8 @@ func (p *playSession) frameDisplayLoop() {
frameCount := 0
lastFrameTime := time.Duration(0)
lastPos := time.Duration(0)
lastPosAt := time.Now()
logging.Info(logging.CatPlayer, "playSession: frameDisplayLoop started (video fps=%.2f, display fps=%.2f, interval=%v)", p.fps, displayFPS, frameDuration)
for {
@ -11515,6 +11524,7 @@ func (p *playSession) frameDisplayLoop() {
if frame == nil {
// No frame available yet - pipeline may be buffering
p.checkStall(lastPos, &lastPosAt)
continue
}
@ -11522,6 +11532,8 @@ func (p *playSession) frameDisplayLoop() {
currentTime := p.gstPlayer.GetCurrentTime()
p.mu.Lock()
isPaused := p.paused
p.lastFrameAt = time.Now()
p.stallCount = 0
p.mu.Unlock()
// Skip if this is the same frame as last time (optimization)
@ -11555,6 +11567,96 @@ func (p *playSession) frameDisplayLoop() {
p.frameFunc(actualFrameNumber)
}
}, false)
lastPos = currentTime
}
}
}
func (p *playSession) checkStall(lastPos time.Duration, lastPosAt *time.Time) {
p.mu.Lock()
if p.paused || p.gstPlayer == nil {
p.mu.Unlock()
return
}
if p.lastFrameAt.IsZero() {
p.lastFrameAt = time.Now()
p.mu.Unlock()
return
}
if time.Since(p.lastFrameAt) < 1500*time.Millisecond {
p.mu.Unlock()
return
}
p.stallCount++
stalls := p.stallCount
p.mu.Unlock()
pos := p.gstPlayer.GetCurrentTime()
now := time.Now()
if pos != lastPos {
*lastPosAt = now
return
}
if now.Sub(*lastPosAt) < 2*time.Second {
return
}
switch stalls {
case 1:
logging.Debug(logging.CatPlayer, "stall watchdog: soft reset (pause/play)")
_ = p.gstPlayer.Pause()
_ = p.gstPlayer.Play()
case 2:
logging.Debug(logging.CatPlayer, "stall watchdog: reseek to %.2fs", p.current)
_ = p.gstPlayer.SeekToTime(time.Duration(p.current * float64(time.Second)))
case 3:
logging.Debug(logging.CatPlayer, "stall watchdog: rebuild pipeline")
p.rebuildPipeline()
default:
if stalls%5 == 0 {
logging.Debug(logging.CatPlayer, "stall watchdog: still stalled (%d attempts)", stalls)
}
}
}
func (p *playSession) rebuildPipeline() {
p.mu.Lock()
path := p.path
w := p.width
h := p.height
fps := p.fps
duration := p.duration
targetW := p.targetW
targetH := p.targetH
volume := p.volume
offset := p.current
img := p.img
prog := p.prog
frameFunc := p.frameFunc
p.mu.Unlock()
if path == "" || w == 0 || h == 0 {
return
}
newSess := newPlaySession(path, w, h, fps, duration, targetW, targetH, prog, frameFunc, img)
if newSess == nil {
return
}
newSess.SetVolume(volume)
_ = newSess.Seek(offset)
newSess.Pause()
p.mu.Lock()
oldStop := p.stop
p.stop = make(chan struct{})
p.mu.Unlock()
if oldStop != nil {
select {
case <-oldStop:
default:
close(oldStop)
}
}
}
@ -11604,6 +11706,8 @@ func (p *playSession) Stop() {
// Use GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.Stop()
p.stallCount = 0
p.lastFrameAt = time.Time{}
close(p.stop)
logging.Debug(logging.CatPlayer, "playSession: Stop called")
return