Fix CRF UI sync and stabilize player
This commit is contained in:
parent
5744f69d28
commit
60fed79840
|
|
@ -12,20 +12,20 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||
"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
|
||||
// and frame-accurate seeking using a single FFmpeg process
|
||||
type UnifiedPlayer struct {
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// FFmpeg process
|
||||
cmd *exec.Cmd
|
||||
stdin *bufio.Writer
|
||||
cmd *exec.Cmd
|
||||
stdin *bufio.Writer
|
||||
stdout *bufio.Reader
|
||||
stderr *bufio.Reader
|
||||
|
||||
|
|
@ -36,30 +36,30 @@ type UnifiedPlayer struct {
|
|||
audioPipeWriter *io.PipeWriter
|
||||
|
||||
// State tracking
|
||||
currentPath string
|
||||
currentTime time.Duration
|
||||
currentPath string
|
||||
currentTime time.Duration
|
||||
currentFrame int64
|
||||
duration time.Duration
|
||||
frameRate float64
|
||||
state PlayerState
|
||||
volume float64
|
||||
speed float64
|
||||
muted bool
|
||||
fullscreen bool
|
||||
previewMode bool
|
||||
duration time.Duration
|
||||
frameRate float64
|
||||
state PlayerState
|
||||
volume float64
|
||||
speed float64
|
||||
muted bool
|
||||
fullscreen bool
|
||||
previewMode bool
|
||||
|
||||
// Video info
|
||||
videoInfo *VideoInfo
|
||||
|
||||
// Synchronization
|
||||
syncClock time.Time
|
||||
videoPTS int64
|
||||
audioPTS int64
|
||||
videoPTS int64
|
||||
audioPTS int64
|
||||
ptsOffset int64
|
||||
|
||||
// Buffer management
|
||||
frameBuffer *sync.Pool
|
||||
audioBuffer []byte
|
||||
frameBuffer *sync.Pool
|
||||
audioBuffer []byte
|
||||
audioBufferSize int
|
||||
|
||||
// Window state
|
||||
|
|
@ -67,7 +67,7 @@ type UnifiedPlayer struct {
|
|||
windowW, windowH int
|
||||
|
||||
// Callbacks
|
||||
timeCallback func(time.Duration)
|
||||
timeCallback func(time.Duration)
|
||||
frameCallback func(int64)
|
||||
stateCallback func(PlayerState)
|
||||
|
||||
|
|
@ -80,14 +80,14 @@ func NewUnifiedPlayer(config Config) *UnifiedPlayer {
|
|||
player := &UnifiedPlayer{
|
||||
config: config,
|
||||
frameBuffer: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &image.RGBA{
|
||||
Pix: make([]uint8, 0),
|
||||
Stride: 0,
|
||||
Rect: image.Rect(0, 0, 0, 0),
|
||||
}
|
||||
New: func() interface{} {
|
||||
return &image.RGBA{
|
||||
Pix: make([]uint8, 0),
|
||||
Stride: 0,
|
||||
Rect: image.Rect(0, 0, 0, 0),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
audioBufferSize: 32768, // 170ms at 48kHz for smooth playback
|
||||
}
|
||||
|
||||
|
|
@ -555,26 +555,25 @@ func (p *UnifiedPlayer) detectVideoProperties() error {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if p.frameRate > 0 && p.duration > 0 {
|
||||
p.videoInfo = &VideoInfo{
|
||||
Width: p.windowW,
|
||||
Height: p.windowH,
|
||||
Duration: p.duration,
|
||||
FrameRate: p.frameRate,
|
||||
Width: p.windowW,
|
||||
Height: p.windowH,
|
||||
Duration: p.duration,
|
||||
FrameRate: p.frameRate,
|
||||
FrameCount: int64(p.duration.Seconds() * p.frameRate),
|
||||
}
|
||||
} else {
|
||||
p.videoInfo = &VideoInfo{
|
||||
Width: p.windowW,
|
||||
Height: p.windowH,
|
||||
Duration: p.duration,
|
||||
FrameRate: p.frameRate,
|
||||
Width: p.windowW,
|
||||
Height: p.windowH,
|
||||
Duration: p.duration,
|
||||
FrameRate: p.frameRate,
|
||||
FrameCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
logging.Debug(logging.CatPlayer, "Video properties: %dx%d@%.3ffps, %.2fs",
|
||||
logging.Debug(logging.CatPlayer, "Video properties: %dx%d@%.3ffps, %.2fs",
|
||||
p.windowW, p.windowH, p.frameRate, p.duration.Seconds())
|
||||
|
||||
return nil
|
||||
|
|
@ -589,13 +588,18 @@ func (p *UnifiedPlayer) writeStringToStdin(cmd string) {
|
|||
|
||||
// updateAVSync maintains synchronization between audio and video
|
||||
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 {
|
||||
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)
|
||||
// Adjust sync clock gradually
|
||||
p.ptsOffset += drift / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -604,7 +608,7 @@ func (p *UnifiedPlayer) updateAVSync() {
|
|||
func (p *UnifiedPlayer) addHardwareAcceleration(args []string) []string {
|
||||
// This is a placeholder - actual implementation would detect available hardware
|
||||
// and add appropriate flags like "-hwaccel cuda", "-c:v h264_nvenc"
|
||||
|
||||
|
||||
// For now, just log that hardware acceleration is considered
|
||||
logging.Debug(logging.CatPlayer, "Hardware acceleration requested but not yet implemented")
|
||||
return args
|
||||
|
|
@ -624,14 +628,14 @@ func (p *UnifiedPlayer) applyVolumeToBuffer(buffer []byte) {
|
|||
if i+1 < len(buffer) {
|
||||
sample := int16(binary.LittleEndian.Uint16(buffer[i : i+2]))
|
||||
adjusted := int(float64(sample) * gain)
|
||||
|
||||
|
||||
// Clamp to int16 range
|
||||
if adjusted > 32767 {
|
||||
adjusted = 32767
|
||||
} else if adjusted < -32768 {
|
||||
adjusted = -32768
|
||||
}
|
||||
|
||||
|
||||
binary.LittleEndian.PutUint16(buffer[i:i+2], uint16(adjusted))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ var (
|
|||
// Audio Codec Colors (Secondary but Distinct)
|
||||
var (
|
||||
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
|
||||
ColorMP3 = utils.MustHex("#F43F5E") // Rose - Legacy audio
|
||||
ColorAC3 = utils.MustHex("#F97316") // Orange-Red - Surround audio
|
||||
|
|
|
|||
34
main.go
34
main.go
|
|
@ -6883,6 +6883,22 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
manualQualityOption := "Manual (CRF)"
|
||||
var crfEntry *widget.Entry
|
||||
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
|
||||
setQuality := func(val string) {
|
||||
|
|
@ -6898,6 +6914,17 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
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 {
|
||||
if state.convert.CRF != "" {
|
||||
state.convert.CRF = ""
|
||||
|
|
@ -7214,7 +7241,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
qualitySectionSimple fyne.CanvasObject
|
||||
qualitySectionAdv fyne.CanvasObject
|
||||
simpleBitrateSelect *widget.Select
|
||||
crfContainer *fyne.Container
|
||||
bitrateContainer *fyne.Container
|
||||
targetSizeContainer *fyne.Container
|
||||
resetConvertDefaults func()
|
||||
|
|
@ -7913,12 +7939,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
"VBR": "VBR (Variable Bitrate)",
|
||||
"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) {
|
||||
// Extract short code from label
|
||||
if shortCode, ok := bitrateModeMap[value]; ok {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user