diff --git a/internal/player/controller_linux.go b/internal/player/controller_linux.go index 3d8f4b0..eceb28d 100644 --- a/internal/player/controller_linux.go +++ b/internal/player/controller_linux.go @@ -1,4 +1,4 @@ -//go:build linux && !gstreamer +//go:build linux && !gstreamer && !gstreamer package player diff --git a/internal/player/controller_stub.go b/internal/player/controller_stub.go index d76dd3a..44d188c 100644 --- a/internal/player/controller_stub.go +++ b/internal/player/controller_stub.go @@ -1,4 +1,4 @@ -//go:build !gstreamer +//go:build !gstreamer && windows package player diff --git a/internal/player/gstreamer_player.go b/internal/player/gstreamer_player.go index 5871803..9e9dc80 100644 --- a/internal/player/gstreamer_player.go +++ b/internal/player/gstreamer_player.go @@ -95,10 +95,12 @@ type GStreamerPlayer struct { volume float64 queued *image.RGBA lastErr string - backend Backend + backend BackendType config Config seekMu sync.Mutex events chan busEvent + preview bool + lastSeekTarget time.Duration mu sync.Mutex @@ -107,13 +109,10 @@ type GStreamerPlayer struct { // Bus handling busCh chan *C.GstMessage - - // Bus loop controls - busStop chan struct{} + eos chan struct{} busDone chan struct{} +} - // Seek coalescing - lastSeekTarget time.Duration } type busEvent struct { @@ -607,6 +606,72 @@ func (p *GStreamerPlayer) busLoop() { 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 { select { case <-p.busQuit: @@ -644,7 +709,7 @@ func (p *GStreamerPlayer) busLoop() { p.pushEvent(evt) case C.GST_MESSAGE_EOS: p.mu.Lock() - p.eos = true + p.eos = true p.mode = StateEOS p.mu.Unlock() p.pushEvent(busEvent{Kind: "eos"}) @@ -671,6 +736,12 @@ func (p *GStreamerPlayer) busLoop() { p.pushEvent(busEvent{Kind: "clock_lost"}) } C.gst_message_unref(msg) + default: + } + p.mu.Unlock() + p.pushEvent(busEvent{Kind: "clock_lost"}) + } + C.gst_message_unref(msg) } } diff --git a/internal/player/gstreamer_player_stub.go b/internal/player/gstreamer_player_stub.go new file mode 100644 index 0000000..b7cfb39 --- /dev/null +++ b/internal/player/gstreamer_player_stub.go @@ -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 } diff --git a/main.go b/main.go index 836b0f5..69eddd5 100644 --- a/main.go +++ b/main.go @@ -768,6 +768,7 @@ type convertConfig struct { AspectUserSet bool // Tracks if user explicitly set OutputAspect ForceAspect bool // Force DAR/SAR metadata even when no aspect conversion TempDir string // Optional temp/cache directory override + Language string // UI language preference ("System" or BCP47 tag) } func (c convertConfig) OutputFile() string { @@ -837,6 +838,7 @@ func defaultConvertConfig() convertConfig { AspectUserSet: false, ForceAspect: true, TempDir: "", + Language: "System", } } @@ -2930,8 +2932,8 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s", encoder, preset) - // Map encoder to hardware acceleration setting - hwAccel := "none" + // Map encoder to hardware acceleration setting only; do not touch codec/preset. + hwAccel := s.convert.HardwareAccel switch { case strings.Contains(encoder, "nvenc"): hwAccel = "nvenc" @@ -2943,26 +2945,11 @@ func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { 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.persistConvertConfig() - dialog.ShowInformation("Benchmark Settings Applied", - fmt.Sprintf("Applied recommended defaults:\n\nEncoder: %s\nPreset: %s\nHardware Accel: %s\n\nThese are now set as your Convert defaults.", - encoder, preset, hwAccel), s.window) + // Minimal notice without confirmation loops. + logging.Info(logging.CatSystem, "benchmark applied hardware acceleration: %s", hwAccel) } func (s *appState) showBenchmarkHistory() {