From 48eaf0be8d1675d66f902c07770df7317fcbbe2b Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Fri, 9 Jan 2026 21:54:55 -0500 Subject: [PATCH] perf(player): optimize frame display loop for smooth playback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance improvements to eliminate choppy playback: 1. Cap display FPS at 30fps: - Even 60fps videos display at max 30fps - Reduces UI update overhead significantly - Human eye can't distinguish >30fps in preview player - Video plays at full 60fps internally, display throttled 2. Skip duplicate frames: - Track lastFrameTime from GStreamer - Only update UI when currentTime changes - Prevents refreshing same frame multiple times - Eliminates the "Frame 389 updated 17 times" issue 3. Remove verbose frame logging: - Removed per-frame debug log (was slowing down UI) - Keep INFO logs for start/stop events - Still log errors when they occur 4. Cleaner logging: - Show both video fps and display fps at startup - Makes performance characteristics visible Results: - Before: Choppy playback, same frame updated repeatedly - After: Smooth 30fps display, no duplicate updates - 4K video (3840x2160) now plays smoothly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- main.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index d5fb556..b5f239b 100644 --- a/main.go +++ b/main.go @@ -11356,12 +11356,18 @@ func (p *playSession) frameDisplayLoop() { if p.fps <= 0 { p.fps = 24 } - frameDuration := time.Second / time.Duration(p.fps) + // Use 30fps max for UI updates to reduce overhead (even for 60fps video) + displayFPS := p.fps + if displayFPS > 30 { + displayFPS = 30 + } + frameDuration := time.Second / time.Duration(displayFPS) ticker := time.NewTicker(frameDuration) defer ticker.Stop() frameCount := 0 - logging.Info(logging.CatPlayer, "playSession: frameDisplayLoop started (fps=%.2f, interval=%v)", p.fps, frameDuration) + lastFrameTime := time.Duration(0) + logging.Info(logging.CatPlayer, "playSession: frameDisplayLoop started (video fps=%.2f, display fps=%.2f, interval=%v)", p.fps, displayFPS, frameDuration) for { select { @@ -11386,11 +11392,19 @@ func (p *playSession) frameDisplayLoop() { continue } + // Get current time from GStreamer + currentTime := p.gstPlayer.GetCurrentTime() + + // Skip if this is the same frame as last time (optimization) + if currentTime == lastFrameTime && frameCount > 0 { + continue + } + lastFrameTime = currentTime + // Update frame counter frameCount++ p.mu.Lock() p.frameN = frameCount - currentTime := p.gstPlayer.GetCurrentTime() p.current = currentTime.Seconds() isPaused := p.paused p.mu.Unlock() @@ -11402,8 +11416,6 @@ func (p *playSession) frameDisplayLoop() { p.img.File = "" p.img.Image = frame p.img.Refresh() - logging.Debug(logging.CatPlayer, "Frame %d updated (%.2fs, paused=%v, size=%dx%d)", - frameCount, p.current, isPaused, frame.Bounds().Dx(), frame.Bounds().Dy()) } if p.prog != nil && !isPaused { p.prog(p.current)