Fix CRF UI sync and stabilize player
This commit is contained in:
parent
6ad6e8ef54
commit
57f2076f9f
|
|
@ -12,20 +12,20 @@ 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
|
||||||
// and frame-accurate seeking using a single FFmpeg process
|
// and frame-accurate seeking using a single FFmpeg process
|
||||||
type UnifiedPlayer struct {
|
type UnifiedPlayer struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
// FFmpeg process
|
// FFmpeg process
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
stdin *bufio.Writer
|
stdin *bufio.Writer
|
||||||
stdout *bufio.Reader
|
stdout *bufio.Reader
|
||||||
stderr *bufio.Reader
|
stderr *bufio.Reader
|
||||||
|
|
||||||
|
|
@ -36,30 +36,30 @@ type UnifiedPlayer struct {
|
||||||
audioPipeWriter *io.PipeWriter
|
audioPipeWriter *io.PipeWriter
|
||||||
|
|
||||||
// State tracking
|
// State tracking
|
||||||
currentPath string
|
currentPath string
|
||||||
currentTime time.Duration
|
currentTime time.Duration
|
||||||
currentFrame int64
|
currentFrame int64
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
frameRate float64
|
frameRate float64
|
||||||
state PlayerState
|
state PlayerState
|
||||||
volume float64
|
volume float64
|
||||||
speed float64
|
speed float64
|
||||||
muted bool
|
muted bool
|
||||||
fullscreen bool
|
fullscreen bool
|
||||||
previewMode bool
|
previewMode bool
|
||||||
|
|
||||||
// Video info
|
// Video info
|
||||||
videoInfo *VideoInfo
|
videoInfo *VideoInfo
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
syncClock time.Time
|
syncClock time.Time
|
||||||
videoPTS int64
|
videoPTS int64
|
||||||
audioPTS int64
|
audioPTS int64
|
||||||
ptsOffset int64
|
ptsOffset int64
|
||||||
|
|
||||||
// Buffer management
|
// Buffer management
|
||||||
frameBuffer *sync.Pool
|
frameBuffer *sync.Pool
|
||||||
audioBuffer []byte
|
audioBuffer []byte
|
||||||
audioBufferSize int
|
audioBufferSize int
|
||||||
|
|
||||||
// Window state
|
// Window state
|
||||||
|
|
@ -67,7 +67,7 @@ type UnifiedPlayer struct {
|
||||||
windowW, windowH int
|
windowW, windowH int
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
timeCallback func(time.Duration)
|
timeCallback func(time.Duration)
|
||||||
frameCallback func(int64)
|
frameCallback func(int64)
|
||||||
stateCallback func(PlayerState)
|
stateCallback func(PlayerState)
|
||||||
|
|
||||||
|
|
@ -80,14 +80,14 @@ func NewUnifiedPlayer(config Config) *UnifiedPlayer {
|
||||||
player := &UnifiedPlayer{
|
player := &UnifiedPlayer{
|
||||||
config: config,
|
config: config,
|
||||||
frameBuffer: &sync.Pool{
|
frameBuffer: &sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
return &image.RGBA{
|
return &image.RGBA{
|
||||||
Pix: make([]uint8, 0),
|
Pix: make([]uint8, 0),
|
||||||
Stride: 0,
|
Stride: 0,
|
||||||
Rect: image.Rect(0, 0, 0, 0),
|
Rect: image.Rect(0, 0, 0, 0),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
audioBufferSize: 32768, // 170ms at 48kHz for smooth playback
|
audioBufferSize: 32768, // 170ms at 48kHz for smooth playback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,26 +555,25 @@ 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,
|
||||||
Height: p.windowH,
|
Height: p.windowH,
|
||||||
Duration: p.duration,
|
Duration: p.duration,
|
||||||
FrameRate: p.frameRate,
|
FrameRate: p.frameRate,
|
||||||
FrameCount: int64(p.duration.Seconds() * p.frameRate),
|
FrameCount: int64(p.duration.Seconds() * p.frameRate),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.videoInfo = &VideoInfo{
|
p.videoInfo = &VideoInfo{
|
||||||
Width: p.windowW,
|
Width: p.windowW,
|
||||||
Height: p.windowH,
|
Height: p.windowH,
|
||||||
Duration: p.duration,
|
Duration: p.duration,
|
||||||
FrameRate: p.frameRate,
|
FrameRate: p.frameRate,
|
||||||
FrameCount: 0,
|
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())
|
p.windowW, p.windowH, p.frameRate, p.duration.Seconds())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -604,7 +608,7 @@ func (p *UnifiedPlayer) updateAVSync() {
|
||||||
func (p *UnifiedPlayer) addHardwareAcceleration(args []string) []string {
|
func (p *UnifiedPlayer) addHardwareAcceleration(args []string) []string {
|
||||||
// This is a placeholder - actual implementation would detect available hardware
|
// This is a placeholder - actual implementation would detect available hardware
|
||||||
// and add appropriate flags like "-hwaccel cuda", "-c:v h264_nvenc"
|
// and add appropriate flags like "-hwaccel cuda", "-c:v h264_nvenc"
|
||||||
|
|
||||||
// For now, just log that hardware acceleration is considered
|
// For now, just log that hardware acceleration is considered
|
||||||
logging.Debug(logging.CatPlayer, "Hardware acceleration requested but not yet implemented")
|
logging.Debug(logging.CatPlayer, "Hardware acceleration requested but not yet implemented")
|
||||||
return args
|
return args
|
||||||
|
|
@ -624,14 +628,14 @@ func (p *UnifiedPlayer) applyVolumeToBuffer(buffer []byte) {
|
||||||
if i+1 < len(buffer) {
|
if i+1 < len(buffer) {
|
||||||
sample := int16(binary.LittleEndian.Uint16(buffer[i : i+2]))
|
sample := int16(binary.LittleEndian.Uint16(buffer[i : i+2]))
|
||||||
adjusted := int(float64(sample) * gain)
|
adjusted := int(float64(sample) * gain)
|
||||||
|
|
||||||
// Clamp to int16 range
|
// Clamp to int16 range
|
||||||
if adjusted > 32767 {
|
if adjusted > 32767 {
|
||||||
adjusted = 32767
|
adjusted = 32767
|
||||||
} else if adjusted < -32768 {
|
} else if adjusted < -32768 {
|
||||||
adjusted = -32768
|
adjusted = -32768
|
||||||
}
|
}
|
||||||
|
|
||||||
binary.LittleEndian.PutUint16(buffer[i:i+2], uint16(adjusted))
|
binary.LittleEndian.PutUint16(buffer[i:i+2], uint16(adjusted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
34
main.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user