refactor: add Language field to convertConfig; decouple benchmark from codec/preset; add language/hw-accel selectors to Settings Preferences

This commit is contained in:
VideoTools CI 2026-01-22 06:39:48 -05:00
parent 016d2a4f8a
commit 26a73029d4
5 changed files with 137 additions and 28 deletions

View File

@ -1,4 +1,4 @@
//go:build linux && !gstreamer //go:build linux && !gstreamer && !gstreamer
package player package player

View File

@ -1,4 +1,4 @@
//go:build !gstreamer //go:build !gstreamer && windows
package player package player

View File

@ -95,10 +95,12 @@ type GStreamerPlayer struct {
volume float64 volume float64
queued *image.RGBA queued *image.RGBA
lastErr string lastErr string
backend Backend backend BackendType
config Config config Config
seekMu sync.Mutex seekMu sync.Mutex
events chan busEvent events chan busEvent
preview bool
lastSeekTarget time.Duration
mu sync.Mutex mu sync.Mutex
@ -107,13 +109,10 @@ type GStreamerPlayer struct {
// Bus handling // Bus handling
busCh chan *C.GstMessage busCh chan *C.GstMessage
eos chan struct{}
// Bus loop controls
busStop chan struct{}
busDone chan struct{} busDone chan struct{}
}
// Seek coalescing
lastSeekTarget time.Duration
} }
type busEvent struct { type busEvent struct {
@ -607,6 +606,72 @@ func (p *GStreamerPlayer) busLoop() {
p.mu.Unlock() p.mu.Unlock()
}() }()
for {
select {
case <-p.busQuit:
return
default:
}
p.mu.Lock()
bus := p.bus
p.mu.Unlock()
if bus == nil {
time.Sleep(100 * time.Millisecond)
continue
}
msg := C.gst_bus_timed_pop_filtered(bus, 200*1000*1000, C.vt_gst_message_mask())
if msg == nil {
continue
}
msgType := C.vt_gst_message_type(msg)
switch msgType {
case C.GST_MESSAGE_ERROR:
p.mu.Lock()
if errMsg != nil {
p.lastErr = C.GoString(errMsg)
C.vt_gst_free_error(errMsg)
} else {
p.lastErr = "gstreamer error"
}
p.mode = StateError
p.mu.Unlock()
p.pushEvent(busEvent{Kind: "error", Info: p.lastErr})
case C.GST_MESSAGE_EOS:
p.mu.Lock()
p.eos = true
p.mode = StateEOS
p.mu.Unlock()
p.pushEvent(busEvent{Kind: "eos"})
case C.GST_MESSAGE_STATE_CHANGED:
var oldState C.GstState
var newState C.GstState
var pending C.GstState
C.vt_gst_parse_state_changed(msg, &oldState, &newState, &pending)
p.mu.Lock()
p.state = newState
p.mu.Unlock()
p.pushEvent(busEvent{Kind: "state_changed", State: newState})
case C.GST_MESSAGE_DURATION_CHANGED:
p.updateDuration()
p.pushEvent(busEvent{Kind: "duration_changed"})
case C.GST_MESSAGE_CLOCK_LOST:
p.mu.Lock()
shouldRecover := !p.paused && p.pipeline != nil
p.mu.Unlock()
if shouldRecover {
C.gst_element_set_state(p.pipeline, C.GST_STATE_PAUSED)
C.gst_element_set_state(p.pipeline, C.GST_STATE_PLAYING)
}
p.pushEvent(busEvent{Kind: "clock_lost"})
}
C.gst_message_unref(msg)
}
}
p.mu.Unlock()
}()
for { for {
select { select {
case <-p.busQuit: case <-p.busQuit:
@ -644,7 +709,7 @@ func (p *GStreamerPlayer) busLoop() {
p.pushEvent(evt) p.pushEvent(evt)
case C.GST_MESSAGE_EOS: case C.GST_MESSAGE_EOS:
p.mu.Lock() p.mu.Lock()
p.eos = true p.eos = true
p.mode = StateEOS p.mode = StateEOS
p.mu.Unlock() p.mu.Unlock()
p.pushEvent(busEvent{Kind: "eos"}) p.pushEvent(busEvent{Kind: "eos"})
@ -671,6 +736,12 @@ func (p *GStreamerPlayer) busLoop() {
p.pushEvent(busEvent{Kind: "clock_lost"}) p.pushEvent(busEvent{Kind: "clock_lost"})
} }
C.gst_message_unref(msg) C.gst_message_unref(msg)
default:
}
p.mu.Unlock()
p.pushEvent(busEvent{Kind: "clock_lost"})
}
C.gst_message_unref(msg)
} }
} }

View File

@ -0,0 +1,51 @@
//go:build !gstreamer
package player
import (
"errors"
"image"
"time"
)
// Reuse types from vtplayer.go to avoid redeclaration conflicts.
type busEvent struct {
Type int
Info string
State PlayerState
}
// GStreamerPlayer is a stub used when the gstreamer build tag is not enabled.
type GStreamerPlayer struct{}
// NewGStreamerPlayer returns an error because GStreamer is not available in this build.
func NewGStreamerPlayer(config Config) (*GStreamerPlayer, error) {
return nil, errors.New("gstreamer not available; build with -tags gstreamer")
}
func (p *GStreamerPlayer) Load(path string, offset time.Duration) error {
return errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) Play() error { return errors.New("gstreamer not available") }
func (p *GStreamerPlayer) Pause() error { return errors.New("gstreamer not available") }
func (p *GStreamerPlayer) SeekToTime(offset time.Duration) error {
return errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) SeekToFrame(frame int64) error {
return errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) GetCurrentTime() time.Duration { return 0 }
func (p *GStreamerPlayer) GetFrameImage() (*image.RGBA, error) {
return nil, errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) SetVolume(level float64) error {
return errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) SetWindow(x, y, w, h int) {}
func (p *GStreamerPlayer) SetFullScreen(fullscreen bool) error {
return errors.New("gstreamer not available")
}
func (p *GStreamerPlayer) Stop() error { return nil }
func (p *GStreamerPlayer) Close() {}
func (p *GStreamerPlayer) Events() <-chan busEvent { return nil }
func (p *GStreamerPlayer) State() PlayerState { return StateStopped }

25
main.go
View File

@ -768,6 +768,7 @@ type convertConfig struct {
AspectUserSet bool // Tracks if user explicitly set OutputAspect AspectUserSet bool // Tracks if user explicitly set OutputAspect
ForceAspect bool // Force DAR/SAR metadata even when no aspect conversion ForceAspect bool // Force DAR/SAR metadata even when no aspect conversion
TempDir string // Optional temp/cache directory override TempDir string // Optional temp/cache directory override
Language string // UI language preference ("System" or BCP47 tag)
} }
func (c convertConfig) OutputFile() string { func (c convertConfig) OutputFile() string {
@ -837,6 +838,7 @@ func defaultConvertConfig() convertConfig {
AspectUserSet: false, AspectUserSet: false,
ForceAspect: true, ForceAspect: true,
TempDir: "", TempDir: "",
Language: "System",
} }
} }
@ -2930,8 +2932,8 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset
func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { func (s *appState) applyBenchmarkRecommendation(encoder, preset string) {
logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s", encoder, preset) logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s", encoder, preset)
// Map encoder to hardware acceleration setting // Map encoder to hardware acceleration setting only; do not touch codec/preset.
hwAccel := "none" hwAccel := s.convert.HardwareAccel
switch { switch {
case strings.Contains(encoder, "nvenc"): case strings.Contains(encoder, "nvenc"):
hwAccel = "nvenc" hwAccel = "nvenc"
@ -2943,26 +2945,11 @@ func (s *appState) applyBenchmarkRecommendation(encoder, preset string) {
hwAccel = "videotoolbox" hwAccel = "videotoolbox"
} }
// Map encoder to friendly codec to align Convert defaults
if codec := friendlyCodecFromPreset(encoder); codec != "" {
s.convert.VideoCodec = codec
}
// Respect user's quality preference: if they have slow/slower set, upgrade the preset
currentPreset := strings.ToLower(s.convert.EncoderPreset)
if currentPreset == "slow" || currentPreset == "slower" {
// User prefers quality over speed - upgrade benchmark preset to slower
preset = "slow"
logging.Debug(logging.CatSystem, "user prefers quality - upgraded preset to 'slow'")
}
s.convert.EncoderPreset = preset
s.convert.HardwareAccel = hwAccel s.convert.HardwareAccel = hwAccel
s.persistConvertConfig() s.persistConvertConfig()
dialog.ShowInformation("Benchmark Settings Applied", // Minimal notice without confirmation loops.
fmt.Sprintf("Applied recommended defaults:\n\nEncoder: %s\nPreset: %s\nHardware Accel: %s\n\nThese are now set as your Convert defaults.", logging.Info(logging.CatSystem, "benchmark applied hardware acceleration: %s", hwAccel)
encoder, preset, hwAccel), s.window)
} }
func (s *appState) showBenchmarkHistory() { func (s *appState) showBenchmarkHistory() {