Apply current convert settings to snippets (scale/aspect/fps/bitrate/preset)

This commit is contained in:
Stu Leak 2025-12-09 01:13:21 -05:00
parent 9245caeb4c
commit a056765673

103
main.go
View File

@ -24,6 +24,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"fyne.io/fyne/v2"
@ -69,6 +70,9 @@ var (
feedbackBundler = utils.NewFeedbackBundler()
appVersion = "v0.1.0-dev14"
hwAccelProbeOnce sync.Once
hwAccelSupported atomic.Value // map[string]bool
modulesList = []Module{
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
@ -202,6 +206,50 @@ func effectiveHardwareAccel(cfg convertConfig) string {
}
}
// hwAccelAvailable checks ffmpeg -hwaccels once and caches the result.
func hwAccelAvailable(accel string) bool {
accel = strings.ToLower(accel)
if accel == "" || accel == "none" {
return false
}
hwAccelProbeOnce.Do(func() {
supported := make(map[string]bool)
cmd := exec.Command("ffmpeg", "-hide_banner", "-v", "error", "-hwaccels")
output, err := cmd.Output()
if err != nil {
hwAccelSupported.Store(supported)
return
}
for _, line := range strings.Split(string(output), "\n") {
line = strings.ToLower(strings.TrimSpace(line))
switch line {
case "cuda":
supported["nvenc"] = true
case "qsv":
supported["qsv"] = true
case "vaapi":
supported["vaapi"] = true
case "videotoolbox":
supported["videotoolbox"] = true
}
}
hwAccelSupported.Store(supported)
})
val := hwAccelSupported.Load()
if val == nil {
return false
}
supported := val.(map[string]bool)
// Treat AMF as available if any GPU accel was detected; ffmpeg -hwaccels may not list it.
if accel == "amf" {
return supported["nvenc"] || supported["qsv"] || supported["vaapi"] || supported["videotoolbox"]
}
return supported[accel]
}
// openLogViewer opens a simple dialog showing the log content. If live is true, it auto-refreshes.
func (s *appState) openLogViewer(title, path string, live bool) {
if strings.TrimSpace(path) == "" {
@ -2910,6 +2958,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
state.convert.OutputAspect = value
updateAspectBoxVisibility()
})
if state.convert.OutputAspect == "" {
state.convert.OutputAspect = "Source"
}
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
// Target File Size with smart presets + manual entry
@ -5286,6 +5337,9 @@ func detectBestH265Encoder() string {
// determineVideoCodec maps user-friendly codec names to FFmpeg codec names
func determineVideoCodec(cfg convertConfig) string {
accel := effectiveHardwareAccel(cfg)
if accel != "" && accel != "none" && !hwAccelAvailable(accel) {
accel = "none"
}
switch cfg.VideoCodec {
case "H.264":
if accel == "nvenc" {
@ -5455,7 +5509,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
}
// Hardware acceleration for decoding (best-effort)
if accel := effectiveHardwareAccel(cfg); accel != "none" && accel != "" {
if accel := effectiveHardwareAccel(cfg); accel != "none" && accel != "" && hwAccelAvailable(accel) {
switch accel {
case "nvenc":
// NVENC encoders handle GPU directly; no hwaccel flag needed
@ -6160,7 +6214,7 @@ func (s *appState) generateSnippet() {
logging.Debug(logging.CatFFMPEG, "snippet: added cover art input %s", s.convert.CoverArtPath)
}
// Build video filters (snippets should be fast - only apply essential filters)
// Build video filters using current settings (respect upscaling/AR/FPS)
var vf []string
// Skip deinterlacing for snippets - they're meant to be fast previews
@ -6178,6 +6232,8 @@ func (s *appState) generateSnippet() {
scaleFilter = "scale=-2:1440"
case "4K":
scaleFilter = "scale=-2:2160"
case "8K":
scaleFilter = "scale=-2:4320"
}
if scaleFilter != "" {
vf = append(vf, scaleFilter)
@ -6197,9 +6253,10 @@ func (s *appState) generateSnippet() {
vf = append(vf, "fps="+s.convert.FrameRate)
}
// WMV files must be re-encoded for MP4 compatibility (wmv3/wmav2 can't be copied to MP4)
// Decide if we must re-encode: filters, non-copy codec, or WMV
isWMV := strings.HasSuffix(strings.ToLower(src.Path), ".wmv")
needsReencode := len(vf) > 0 || isWMV
forcedCodec := !strings.EqualFold(s.convert.VideoCodec, "Copy")
needsReencode := len(vf) > 0 || isWMV || forcedCodec
if len(vf) > 0 {
filterStr := strings.Join(vf, ",")
@ -6221,23 +6278,41 @@ func (s *appState) generateSnippet() {
args = append(args, "-c:v", "copy")
}
} else {
// Filters required - must re-encode
// Use configured codec or fallback to H.264 for compatibility
// Filters/codec require re-encode; use current settings
videoCodec := determineVideoCodec(s.convert)
if videoCodec == "copy" {
videoCodec = "libx264"
}
args = append(args, "-c:v", videoCodec)
// Use configured CRF or fallback to quality preset
crf := s.convert.CRF
if crf == "" {
crf = crfForQuality(s.convert.Quality)
// Bitrate/quality from current mode
mode := s.convert.BitrateMode
if mode == "" {
mode = "CRF"
}
if videoCodec == "libx264" || videoCodec == "libx265" {
args = append(args, "-crf", crf)
// Use faster preset for snippets
args = append(args, "-preset", "veryfast")
switch mode {
case "CBR", "VBR":
vb := s.convert.VideoBitrate
if vb == "" {
vb = defaultBitrate(s.convert.VideoCodec, src.Width, src.Bitrate)
}
args = append(args, "-b:v", vb)
if mode == "CBR" {
args = append(args, "-minrate", vb, "-maxrate", vb, "-bufsize", vb)
}
default: // CRF/Target size fallback to CRF
crf := s.convert.CRF
if crf == "" {
crf = crfForQuality(s.convert.Quality)
}
if videoCodec == "libx264" || videoCodec == "libx265" {
args = append(args, "-crf", crf)
}
}
// Preset from current settings
if s.convert.EncoderPreset != "" && (strings.Contains(videoCodec, "264") || strings.Contains(videoCodec, "265")) {
args = append(args, "-preset", s.convert.EncoderPreset)
}
// Pixel format