Performance Analysis: - Created PLAYER_PERFORMANCE_ISSUES.md with root cause analysis - Identified 6 major issues causing stuttering - Provided priority-ordered fix recommendations Quick Fixes Implemented (Priority 1): - Increase audio buffer: 2048 → 8192 samples (42ms → 170ms) - Increase audio chunk size: 4096 → 16384 bytes (21ms → 85ms) - Reduces audio underruns and stuttering significantly Root Causes Documented: 1. Separate video/audio FFmpeg processes (no A/V sync) 2. Tiny audio buffers causing underruns 3. CPU waste on per-sample volume processing 4. Frame timing drift with no correction mechanism 5. UI thread blocking every frame update 6. Memory allocation on every frame (GC pressure) Remaining Work (Requires More Time): - Priority 2: Move volume control to FFmpeg (remove hot path processing) - Priority 3: Single FFmpeg process for perfect A/V sync - Priority 4: Frame buffer pooling to reduce GC pressure - Priority 5: Adaptive frame timing with drift correction Testing Checklist Provided: - Frame rate support (24/30/60 fps) - A/V sync validation - Codec compatibility - CPU usage benchmarking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
355 lines
8.7 KiB
Markdown
355 lines
8.7 KiB
Markdown
# Player Module Performance Issues & Fixes
|
|
|
|
## Current Problems Causing Stuttering
|
|
|
|
### 1. **Separate Video & Audio Processes (No Sync)**
|
|
**Location:** `main.go:9144` (runVideo) and `main.go:9233` (runAudio)
|
|
|
|
**Problem:**
|
|
- Video and audio run in completely separate FFmpeg processes
|
|
- No synchronization mechanism between them
|
|
- They will inevitably drift apart, causing A/V desync and stuttering
|
|
|
|
**Current Implementation:**
|
|
```go
|
|
func (p *playSession) startLocked(offset float64) {
|
|
p.runVideo(offset) // Separate process
|
|
p.runAudio(offset) // Separate process
|
|
}
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- If video frame processing takes too long → audio continues → desync
|
|
- If audio buffer underruns → video continues → desync
|
|
- No feedback loop to keep them in sync
|
|
|
|
---
|
|
|
|
### 2. **Audio Buffer Too Small**
|
|
**Location:** `main.go:8960` (audio context) and `main.go:9274` (chunk size)
|
|
|
|
**Problem:**
|
|
```go
|
|
// Audio context with tiny buffer (42ms at 48kHz)
|
|
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
|
|
|
// Tiny read chunks (21ms of audio)
|
|
chunk := make([]byte, 4096)
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- 21ms chunks mean we need to read 47 times per second
|
|
- Any delay > 21ms causes audio dropout/stuttering
|
|
- 2048 sample buffer gives only 42ms protection against underruns
|
|
- Modern systems need 100-200ms buffers for smooth playback
|
|
|
|
---
|
|
|
|
### 3. **Volume Processing in Hot Path**
|
|
**Location:** `main.go:9294-9318`
|
|
|
|
**Problem:**
|
|
```go
|
|
// Processes volume on EVERY audio chunk read
|
|
for i := 0; i+1 < n; i += 2 {
|
|
sample := int16(binary.LittleEndian.Uint16(tmp[i:]))
|
|
amp := int(float64(sample) * gain)
|
|
// ... clamping ...
|
|
binary.LittleEndian.PutUint16(tmp[i:], uint16(int16(amp)))
|
|
}
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- CPU-intensive per-sample processing
|
|
- Happens 47 times/second with tiny chunks
|
|
- Blocks the audio read loop
|
|
- Should use FFmpeg's volume filter or hardware mixing
|
|
|
|
---
|
|
|
|
### 4. **Video Frame Pacing Issues**
|
|
**Location:** `main.go:9200-9203`
|
|
|
|
**Problem:**
|
|
```go
|
|
if delay := time.Until(nextFrameAt); delay > 0 {
|
|
time.Sleep(delay)
|
|
}
|
|
nextFrameAt = nextFrameAt.Add(frameDur)
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- `time.Sleep()` is not precise (can wake up late)
|
|
- Cumulative drift: if one frame is late, all future frames shift
|
|
- No correction mechanism if we fall behind
|
|
- UI thread delays from `DoFromGoroutine` can cause frame drops
|
|
|
|
---
|
|
|
|
### 5. **UI Thread Blocking**
|
|
**Location:** `main.go:9207-9215`
|
|
|
|
**Problem:**
|
|
```go
|
|
// Every frame waits for UI thread to be available
|
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
p.img.Image = frame
|
|
p.img.Refresh()
|
|
}, false)
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- If UI thread is busy, frame updates queue up
|
|
- Can cause video to appear choppy even if FFmpeg is delivering smoothly
|
|
- No frame dropping mechanism if UI can't keep up
|
|
|
|
---
|
|
|
|
### 6. **Frame Allocation on Every Frame**
|
|
**Location:** `main.go:9205-9206`
|
|
|
|
**Problem:**
|
|
```go
|
|
// Allocates new frame buffer 24-60 times per second
|
|
frame := image.NewRGBA(image.Rect(0, 0, p.targetW, p.targetH))
|
|
utils.CopyRGBToRGBA(frame.Pix, buf)
|
|
```
|
|
|
|
**Why It Stutters:**
|
|
- Memory allocation on every frame causes GC pressure
|
|
- Extra copy operation adds latency
|
|
- Could reuse buffers or use ring buffer
|
|
|
|
---
|
|
|
|
## Recommended Fixes (Priority Order)
|
|
|
|
### Priority 1: Increase Audio Buffers (Quick Fix)
|
|
|
|
**Change `main.go:8960`:**
|
|
```go
|
|
// OLD: 2048 samples = 42ms
|
|
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
|
|
|
// NEW: 8192 samples = 170ms (more buffer = smoother playback)
|
|
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 8192)
|
|
```
|
|
|
|
**Change `main.go:9274`:**
|
|
```go
|
|
// OLD: 4096 bytes = 21ms
|
|
chunk := make([]byte, 4096)
|
|
|
|
// NEW: 16384 bytes = 85ms per chunk
|
|
chunk := make([]byte, 16384)
|
|
```
|
|
|
|
**Expected Result:** Audio stuttering should improve significantly
|
|
|
|
---
|
|
|
|
### Priority 2: Use FFmpeg for Volume Control
|
|
|
|
**Change `main.go:9238-9247`:**
|
|
```go
|
|
// Add volume filter to FFmpeg command instead of processing in Go
|
|
volumeFilter := ""
|
|
if p.muted || p.volume <= 0 {
|
|
volumeFilter = "-af volume=0"
|
|
} else if math.Abs(p.volume - 100) > 0.1 {
|
|
volumeFilter = fmt.Sprintf("-af volume=%.2f", p.volume/100.0)
|
|
}
|
|
|
|
cmd := exec.Command(platformConfig.FFmpegPath,
|
|
"-hide_banner", "-loglevel", "error",
|
|
"-ss", fmt.Sprintf("%.3f", offset),
|
|
"-i", p.path,
|
|
"-vn",
|
|
"-ac", fmt.Sprintf("%d", channels),
|
|
"-ar", fmt.Sprintf("%d", sampleRate),
|
|
volumeFilter, // Let FFmpeg handle volume
|
|
"-f", "s16le",
|
|
"-",
|
|
)
|
|
```
|
|
|
|
**Remove volume processing loop (lines 9294-9318):**
|
|
```go
|
|
// Simply write chunks directly
|
|
localPlayer.Write(chunk[:n])
|
|
```
|
|
|
|
**Expected Result:** Reduced CPU usage, smoother audio
|
|
|
|
---
|
|
|
|
### Priority 3: Use Single FFmpeg Process with A/V Sync
|
|
|
|
**Conceptual Change:**
|
|
Instead of separate video/audio processes, use ONE FFmpeg process that:
|
|
1. Outputs video frames to one pipe
|
|
2. Outputs audio to another pipe (or use `-f matroska` with demuxing)
|
|
3. Maintains sync internally
|
|
|
|
**Pseudocode:**
|
|
```go
|
|
cmd := exec.Command(platformConfig.FFmpegPath,
|
|
"-ss", fmt.Sprintf("%.3f", offset),
|
|
"-i", p.path,
|
|
// Video stream
|
|
"-map", "0:v:0",
|
|
"-f", "rawvideo",
|
|
"-pix_fmt", "rgb24",
|
|
"-r", fmt.Sprintf("%.3f", p.fps),
|
|
"pipe:4", // Video to fd 4
|
|
// Audio stream
|
|
"-map", "0:a:0",
|
|
"-ac", "2",
|
|
"-ar", "48000",
|
|
"-f", "s16le",
|
|
"pipe:5", // Audio to fd 5
|
|
)
|
|
```
|
|
|
|
**Expected Result:** Perfect A/V sync, no drift
|
|
|
|
---
|
|
|
|
### Priority 4: Frame Buffer Reuse
|
|
|
|
**Change `main.go:9205-9206`:**
|
|
```go
|
|
// Reuse frame buffers instead of allocating every frame
|
|
type framePool struct {
|
|
pool sync.Pool
|
|
}
|
|
|
|
func (p *framePool) get(w, h int) *image.RGBA {
|
|
if img := p.pool.Get(); img != nil {
|
|
return img.(*image.RGBA)
|
|
}
|
|
return image.NewRGBA(image.Rect(0, 0, w, h))
|
|
}
|
|
|
|
func (p *framePool) put(img *image.RGBA) {
|
|
// Clear pixel data
|
|
for i := range img.Pix {
|
|
img.Pix[i] = 0
|
|
}
|
|
p.pool.Put(img)
|
|
}
|
|
|
|
// In video loop:
|
|
frame := framePool.get(p.targetW, p.targetH)
|
|
utils.CopyRGBToRGBA(frame.Pix, buf)
|
|
// ... use frame ...
|
|
// Note: can't return to pool if UI is still using it
|
|
```
|
|
|
|
**Expected Result:** Reduced GC pressure, smoother frame delivery
|
|
|
|
---
|
|
|
|
### Priority 5: Adaptive Frame Timing
|
|
|
|
**Change `main.go:9200-9203`:**
|
|
```go
|
|
// Track actual vs expected time to detect drift
|
|
now := time.Now()
|
|
behind := now.Sub(nextFrameAt)
|
|
|
|
if behind < 0 {
|
|
// We're ahead, sleep until next frame
|
|
time.Sleep(-behind)
|
|
} else if behind > frameDur*2 {
|
|
// We're way behind (>2 frames), skip this frame
|
|
logging.Debug(logging.CatFFMPEG, "dropping frame, %.0fms behind", behind.Seconds()*1000)
|
|
nextFrameAt = now
|
|
continue
|
|
} else {
|
|
// We're slightly behind, catchup gradually
|
|
nextFrameAt = now.Add(frameDur / 2)
|
|
}
|
|
|
|
nextFrameAt = nextFrameAt.Add(frameDur)
|
|
```
|
|
|
|
**Expected Result:** Better handling of temporary slowdowns, adaptive recovery
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
After each fix, test:
|
|
|
|
- [ ] 24fps video plays smoothly
|
|
- [ ] 30fps video plays smoothly
|
|
- [ ] 60fps video plays smoothly
|
|
- [ ] Audio doesn't stutter
|
|
- [ ] A/V sync maintained over 30+ seconds
|
|
- [ ] Seeking doesn't cause prolonged stuttering
|
|
- [ ] CPU usage is reasonable (<20% for playback)
|
|
- [ ] Works on both Linux and Windows
|
|
- [ ] Works with various codecs (H.264, H.265, VP9)
|
|
- [ ] Volume control works smoothly
|
|
- [ ] Pause/resume doesn't cause issues
|
|
|
|
---
|
|
|
|
## Performance Monitoring
|
|
|
|
Add instrumentation to measure:
|
|
|
|
```go
|
|
// Video frame timing
|
|
frameDeliveryTime := time.Since(frameReadStart)
|
|
if frameDeliveryTime > frameDur*1.5 {
|
|
logging.Debug(logging.CatFFMPEG, "slow frame delivery: %.1fms (target: %.1fms)",
|
|
frameDeliveryTime.Seconds()*1000,
|
|
frameDur.Seconds()*1000)
|
|
}
|
|
|
|
// Audio buffer health
|
|
if audioBufferFillLevel < 0.3 {
|
|
logging.Debug(logging.CatFFMPEG, "audio buffer low: %.0f%%", audioBufferFillLevel*100)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Alternative: Use External Player Library
|
|
|
|
If these tweaks don't achieve smooth playback, consider:
|
|
|
|
1. **mpv library** (libmpv) - Industry standard, perfect A/V sync
|
|
2. **FFmpeg's ffplay** code - Reference implementation
|
|
3. **VLC libvlc** - Proven playback engine
|
|
|
|
These handle all the complex synchronization automatically.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Root Causes:**
|
|
1. Separate video/audio processes with no sync
|
|
2. Tiny audio buffers causing underruns
|
|
3. CPU waste on per-sample volume processing
|
|
4. Frame timing drift with no correction
|
|
5. UI thread blocking frame updates
|
|
|
|
**Quick Wins (30 min):**
|
|
- Increase audio buffers (Priority 1)
|
|
- Move volume to FFmpeg (Priority 2)
|
|
|
|
**Proper Fix (2-4 hours):**
|
|
- Single FFmpeg process with A/V muxing (Priority 3)
|
|
- Frame buffer pooling (Priority 4)
|
|
- Adaptive timing (Priority 5)
|
|
|
|
**Expected Final Result:**
|
|
- Smooth playback at all frame rates
|
|
- Rock-solid A/V sync
|
|
- Low CPU usage
|
|
- No stuttering or dropouts
|