Add player robustness improvements and A/V sync logging

Improvements:
1. Track audio active state with atomic.Bool flag
2. Handle videos without audio track gracefully
   - If audio fails to start, video plays at natural frame rate
   - Clear error messages indicate "video-only playback"
3. Better A/V sync logging for debugging
   - Log when video ahead/behind and actions taken
   - Log good sync status periodically (every ~6 seconds at 30fps)
   - More granular logging for different sync states
4. Proper cleanup when audio stream ends or fails

How it works:
- audioActive flag set to true when audio starts successfully
- Set to false when audio fails to start or ends
- Video checks audioActive before syncing to audio clock
- If no audio: video just paces at natural frame rate (no sync)
- If audio active: full A/V sync with adaptive timing

Expected improvements:
- Video-only files (GIFs, silent videos) play smoothly
- Better debugging info for sync quality
- Graceful degradation when audio missing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-24 01:44:08 -05:00
parent e896fd086d
commit 6729e98fae

94
main.go
View File

@ -9288,52 +9288,57 @@ func (p *playSession) runVideo(offset float64) {
return
}
// Sync video to audio master clock
// Sync video to audio master clock (if audio is active)
videoFrameTime := offset + (float64(p.frameN) / p.fps)
p.videoTime = videoFrameTime
// Get audio clock time
var audioClockTime float64
if audioTimeVal := p.audioTime.Load(); audioTimeVal != nil {
audioClockTime = audioTimeVal.(float64)
} else {
audioClockTime = videoFrameTime
}
if p.audioActive.Load() {
// Audio is active - sync video to audio master clock
var audioClockTime float64
if audioTimeVal := p.audioTime.Load(); audioTimeVal != nil {
audioClockTime = audioTimeVal.(float64)
} else {
audioClockTime = videoFrameTime
}
// Calculate A/V sync difference
avDiff := videoFrameTime - audioClockTime
// Calculate A/V sync difference
avDiff := videoFrameTime - audioClockTime
// Adaptive timing based on A/V sync
if avDiff < -frameDur.Seconds()*3 {
// Video is way ahead of audio (>3 frames) - wait longer
time.Sleep(frameDur * 2)
if p.frameN%30 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video ahead %.0fms, slowing down", -avDiff*1000)
// Adaptive timing based on A/V sync
if avDiff < -frameDur.Seconds()*3 {
// Video is way ahead of audio (>3 frames) - wait longer
time.Sleep(frameDur * 2)
if p.frameN%30 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video ahead %.0fms, slowing down", -avDiff*1000)
}
} else if avDiff > frameDur.Seconds()*3 {
// Video is way behind audio (>3 frames) - drop frame
if p.frameN%30 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video behind %.0fms, dropping frame", avDiff*1000)
}
p.frameN++
p.current = offset + (float64(p.frameN) / p.fps)
continue
} else if avDiff > frameDur.Seconds() {
// Video slightly behind - speed up (skip sleep)
if p.frameN%60 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video slightly behind %.0fms, catching up", avDiff*1000)
}
} else if avDiff < -frameDur.Seconds() {
// Video slightly ahead - slow down
if p.frameN%60 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video slightly ahead %.0fms, waiting", -avDiff*1000)
}
time.Sleep(frameDur + time.Duration(math.Abs(avDiff)*float64(time.Second)))
} else {
// In sync - normal timing
if p.frameN%180 == 0 && p.frameN > 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: good sync (diff %.1fms)", avDiff*1000)
}
time.Sleep(frameDur)
}
} else if avDiff > frameDur.Seconds()*3 {
// Video is way behind audio (>3 frames) - drop frame
if p.frameN%30 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video behind %.0fms, dropping frame", avDiff*1000)
}
p.frameN++
p.current = offset + (float64(p.frameN) / p.fps)
continue
} else if avDiff > frameDur.Seconds() {
// Video slightly behind - speed up (skip sleep)
if p.frameN%60 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video slightly behind %.0fms, catching up", avDiff*1000)
}
} else if avDiff < -frameDur.Seconds() {
// Video slightly ahead - slow down
if p.frameN%60 == 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: video slightly ahead %.0fms, waiting", -avDiff*1000)
}
time.Sleep(frameDur + time.Duration(math.Abs(avDiff)*float64(time.Second)))
} else {
// In sync - normal timing
if p.frameN%180 == 0 && p.frameN > 0 {
logging.Debug(logging.CatFFMPEG, "A/V sync: good sync (diff %.1fms)", avDiff*1000)
}
// No audio - just pace video at its natural frame rate
time.Sleep(frameDur)
}
// Allocate a fresh frame to avoid concurrent texture reuse issues.
@ -9404,24 +9409,29 @@ func (p *playSession) runAudio(offset float64) {
return
}
if err := cmd.Start(); err != nil {
logging.Debug(logging.CatFFMPEG, "audio start failed: %v (%s)", err, strings.TrimSpace(stderr.String()))
logging.Debug(logging.CatFFMPEG, "audio start failed (video-only playback): %v (%s)", err, strings.TrimSpace(stderr.String()))
p.audioActive.Store(false)
return
}
p.audioCmd = cmd
ctx, err := getAudioContext(sampleRate, channels, bytesPerSample)
if err != nil {
logging.Debug(logging.CatFFMPEG, "audio context error: %v", err)
logging.Debug(logging.CatFFMPEG, "audio context error (video-only playback): %v", err)
p.audioActive.Store(false)
return
}
player := ctx.NewPlayer()
if player == nil {
logging.Debug(logging.CatFFMPEG, "audio player creation failed")
logging.Debug(logging.CatFFMPEG, "audio player creation failed (video-only playback)")
p.audioActive.Store(false)
return
}
p.audioActive.Store(true) // Mark audio as active
localPlayer := player
go func() {
defer cmd.Process.Kill()
defer localPlayer.Close()
defer p.audioActive.Store(false) // Mark audio as inactive when done
// Increased from 4096 (21ms) to 16384 (85ms) for smoother playback
// Larger chunks reduce read frequency and improve performance
chunk := make([]byte, 16384)