Fix CRF UI sync and stabilize player

This commit is contained in:
Stu Leak 2026-01-04 16:45:08 -05:00
parent 6ad6e8ef54
commit 57f2076f9f
3 changed files with 76 additions and 52 deletions

View File

@ -12,8 +12,8 @@ import (
"sync" "sync"
"time" "time"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging" "git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
) )
// UnifiedPlayer implements rock-solid video playback with proper A/V synchronization // UnifiedPlayer implements rock-solid video playback with proper A/V synchronization
@ -555,7 +555,6 @@ func (p *UnifiedPlayer) detectVideoProperties() error {
} }
} }
if p.frameRate > 0 && p.duration > 0 { if p.frameRate > 0 && p.duration > 0 {
p.videoInfo = &VideoInfo{ p.videoInfo = &VideoInfo{
Width: p.windowW, Width: p.windowW,
@ -589,13 +588,18 @@ func (p *UnifiedPlayer) writeStringToStdin(cmd string) {
// updateAVSync maintains synchronization between audio and video // updateAVSync maintains synchronization between audio and video
func (p *UnifiedPlayer) updateAVSync() { func (p *UnifiedPlayer) updateAVSync() {
// Simple drift correction using master clock reference // PTS-based drift correction with adaptive timing
p.mu.RLock()
defer p.mu.RUnlock()
if p.audioPTS > 0 && p.videoPTS > 0 { if p.audioPTS > 0 && p.videoPTS > 0 {
drift := p.audioPTS - p.videoPTS drift := p.audioPTS - p.videoPTS
if abs(drift) > 1000 { // More than 1 frame of drift if abs(drift) > 900 { // More than 10ms of drift (at 90kHz)
logging.Debug(logging.CatPlayer, "A/V sync drift: %d PTS", drift)
// Gradual adjustment to avoid audio glitches
p.ptsOffset += drift / 10 // 10% correction per frame
} else {
logging.Debug(logging.CatPlayer, "A/V sync drift: %d PTS", drift) logging.Debug(logging.CatPlayer, "A/V sync drift: %d PTS", drift)
// Adjust sync clock gradually
p.ptsOffset += drift / 100
} }
} }
} }

View File

@ -44,7 +44,7 @@ var (
// Audio Codec Colors (Secondary but Distinct) // Audio Codec Colors (Secondary but Distinct)
var ( var (
ColorOpus = utils.MustHex("#8B5CF6") // Violet - Modern audio ColorOpus = utils.MustHex("#8B5CF6") // Violet - Modern audio
ColorAAC = utils.MustHex("#7C3AED") // Purple-Blue - Common audio ColorAAC = utils.MustHex("#06B6D4") // Cyan - Common audio (distinct from purple codecs)
ColorFLAC = utils.MustHex("#EC4899") // Magenta - Lossless audio ColorFLAC = utils.MustHex("#EC4899") // Magenta - Lossless audio
ColorMP3 = utils.MustHex("#F43F5E") // Rose - Legacy audio ColorMP3 = utils.MustHex("#F43F5E") // Rose - Legacy audio
ColorAC3 = utils.MustHex("#F97316") // Orange-Red - Surround audio ColorAC3 = utils.MustHex("#F97316") // Orange-Red - Surround audio

34
main.go
View File

@ -6883,6 +6883,22 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
manualQualityOption := "Manual (CRF)" manualQualityOption := "Manual (CRF)"
var crfEntry *widget.Entry var crfEntry *widget.Entry
var manualCrfRow *fyne.Container var manualCrfRow *fyne.Container
var crfContainer *fyne.Container
normalizeBitrateMode := func(mode string) string {
switch {
case strings.HasPrefix(mode, "CRF"):
return "CRF"
case strings.HasPrefix(mode, "CBR"):
return "CBR"
case strings.HasPrefix(mode, "VBR"):
return "VBR"
case strings.HasPrefix(mode, "Target Size"):
return "Target Size"
default:
return mode
}
}
// State setters with automatic widget synchronization // State setters with automatic widget synchronization
setQuality := func(val string) { setQuality := func(val string) {
@ -6898,6 +6914,17 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
crfEntry.SetText("23") crfEntry.SetText("23")
} }
} }
if normalizeBitrateMode(state.convert.BitrateMode) == "CRF" {
if manualCrfRow != nil {
manualCrfRow.Show()
}
if crfEntry != nil {
crfEntry.Enable()
}
if crfContainer != nil {
crfContainer.Show()
}
}
} else { } else {
if state.convert.CRF != "" { if state.convert.CRF != "" {
state.convert.CRF = "" state.convert.CRF = ""
@ -7214,7 +7241,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
qualitySectionSimple fyne.CanvasObject qualitySectionSimple fyne.CanvasObject
qualitySectionAdv fyne.CanvasObject qualitySectionAdv fyne.CanvasObject
simpleBitrateSelect *widget.Select simpleBitrateSelect *widget.Select
crfContainer *fyne.Container
bitrateContainer *fyne.Container bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container targetSizeContainer *fyne.Container
resetConvertDefaults func() resetConvertDefaults func()
@ -7913,12 +7939,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
"VBR": "VBR (Variable Bitrate)", "VBR": "VBR (Variable Bitrate)",
"Target Size": "Target Size (Calculate from file size)", "Target Size": "Target Size (Calculate from file size)",
} }
normalizeBitrateMode := func(mode string) string {
if shortCode, ok := bitrateModeMap[mode]; ok {
return shortCode
}
return mode
}
bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) { bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) {
// Extract short code from label // Extract short code from label
if shortCode, ok := bitrateModeMap[value]; ok { if shortCode, ok := bitrateModeMap[value]; ok {