Expand convert presets and relative scaling

This commit is contained in:
Stu Leak 2025-12-13 23:08:54 -05:00
parent 43eae3d17e
commit 3f356f9a74

197
main.go
View File

@ -513,12 +513,12 @@ func savePersistedConvertConfig(cfg convertConfig) error {
// benchmarkRun represents a single benchmark test run // benchmarkRun represents a single benchmark test run
type benchmarkRun struct { type benchmarkRun struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Results []benchmark.Result `json:"results"` Results []benchmark.Result `json:"results"`
RecommendedEncoder string `json:"recommended_encoder"` RecommendedEncoder string `json:"recommended_encoder"`
RecommendedPreset string `json:"recommended_preset"` RecommendedPreset string `json:"recommended_preset"`
RecommendedHWAccel string `json:"recommended_hwaccel"` RecommendedHWAccel string `json:"recommended_hwaccel"`
RecommendedFPS float64 `json:"recommended_fps"` RecommendedFPS float64 `json:"recommended_fps"`
} }
// benchmarkConfig holds benchmark history // benchmarkConfig holds benchmark history
@ -566,47 +566,47 @@ func saveBenchmarkConfig(cfg benchmarkConfig) error {
} }
type appState struct { type appState struct {
window fyne.Window window fyne.Window
active string active string
lastModule string lastModule string
source *videoSource source *videoSource
loadedVideos []*videoSource // Multiple loaded videos for navigation loadedVideos []*videoSource // Multiple loaded videos for navigation
currentIndex int // Current video index in loadedVideos currentIndex int // Current video index in loadedVideos
anim *previewAnimator anim *previewAnimator
convert convertConfig convert convertConfig
currentFrame string currentFrame string
player player.Controller player player.Controller
playerReady bool playerReady bool
playerVolume float64 playerVolume float64
playerMuted bool playerMuted bool
lastVolume float64 lastVolume float64
playerPaused bool playerPaused bool
playerPos float64 playerPos float64
playerLast time.Time playerLast time.Time
progressQuit chan struct{} progressQuit chan struct{}
convertCancel context.CancelFunc convertCancel context.CancelFunc
playerSurf *playerSurface playerSurf *playerSurface
convertBusy bool convertBusy bool
convertStatus string convertStatus string
convertActiveIn string convertActiveIn string
convertActiveOut string convertActiveOut string
convertActiveLog string convertActiveLog string
convertProgress float64 convertProgress float64
convertFPS float64 convertFPS float64
convertSpeed float64 convertSpeed float64
convertETA time.Duration convertETA time.Duration
playSess *playSession playSess *playSession
jobQueue *queue.Queue jobQueue *queue.Queue
statsBar *ui.ConversionStatsBar statsBar *ui.ConversionStatsBar
queueBtn *widget.Button queueBtn *widget.Button
queueScroll *container.Scroll queueScroll *container.Scroll
queueOffset fyne.Position queueOffset fyne.Position
compareFile1 *videoSource compareFile1 *videoSource
compareFile2 *videoSource compareFile2 *videoSource
inspectFile *videoSource inspectFile *videoSource
inspectInterlaceResult *interlace.DetectionResult inspectInterlaceResult *interlace.DetectionResult
inspectInterlaceAnalyzing bool inspectInterlaceAnalyzing bool
autoCompare bool // Auto-load Compare module after conversion autoCompare bool // Auto-load Compare module after conversion
// Merge state // Merge state
mergeClips []mergeClip mergeClips []mergeClip
@ -626,8 +626,8 @@ type appState struct {
thumbLastOutputPath string // Path to last generated output thumbLastOutputPath string // Path to last generated output
// Interlacing detection state // Interlacing detection state
interlaceResult *interlace.DetectionResult interlaceResult *interlace.DetectionResult
interlaceAnalyzing bool interlaceAnalyzing bool
} }
type mergeClip struct { type mergeClip struct {
@ -1363,10 +1363,10 @@ func (s *appState) detectHardwareEncoders() []string {
// Check for hardware encoders by trying to get codec info // Check for hardware encoders by trying to get codec info
encodersToCheck := []string{ encodersToCheck := []string{
"h264_nvenc", "hevc_nvenc", // NVIDIA "h264_nvenc", "hevc_nvenc", // NVIDIA
"h264_qsv", "hevc_qsv", // Intel QuickSync "h264_qsv", "hevc_qsv", // Intel QuickSync
"h264_amf", "hevc_amf", // AMD AMF "h264_amf", "hevc_amf", // AMD AMF
"h264_videotoolbox", // Apple VideoToolbox "h264_videotoolbox", // Apple VideoToolbox
} }
for _, encoder := range encodersToCheck { for _, encoder := range encodersToCheck {
@ -2089,16 +2089,16 @@ func (s *appState) showMergeView() {
} }
formatMap := map[string]string{ formatMap := map[string]string{
"MKV (Copy streams)": "mkv-copy", "MKV (Copy streams)": "mkv-copy",
"MKV (H.264)": "mkv-h264", "MKV (H.264)": "mkv-h264",
"MKV (H.265)": "mkv-h265", "MKV (H.265)": "mkv-h265",
"MP4 (H.264)": "mp4-h264", "MP4 (H.264)": "mp4-h264",
"MP4 (H.265)": "mp4-h265", "MP4 (H.265)": "mp4-h265",
"DVD NTSC 16:9": "dvd-ntsc-169", "DVD NTSC 16:9": "dvd-ntsc-169",
"DVD NTSC 4:3": "dvd-ntsc-43", "DVD NTSC 4:3": "dvd-ntsc-43",
"DVD PAL 16:9": "dvd-pal-169", "DVD PAL 16:9": "dvd-pal-169",
"DVD PAL 4:3": "dvd-pal-43", "DVD PAL 4:3": "dvd-pal-43",
"Blu-ray (H.264)": "bd-h264", "Blu-ray (H.264)": "bd-h264",
} }
var formatKeys []string var formatKeys []string
for k := range formatMap { for k := range formatMap {
@ -2210,10 +2210,10 @@ func (s *appState) showMergeView() {
) )
left := container.NewBorder( left := container.NewBorder(
leftTop, // top leftTop, // top
nil, // bottom nil, // bottom
nil, // left nil, // left
nil, // right nil, // right
ui.NewDroppable(listScroll, func(items []fyne.URI) { ui.NewDroppable(listScroll, func(items []fyne.URI) {
var paths []string var paths []string
for _, uri := range items { for _, uri := range items {
@ -3297,7 +3297,7 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
"-i", inputPath, "-i", inputPath,
"-t", "20", "-t", "20",
"-c", "copy", // Copy all streams without re-encoding "-c", "copy", // Copy all streams without re-encoding
"-map", "0", // Include all streams "-map", "0", // Include all streams
outputPath, outputPath,
} }
@ -3757,7 +3757,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
updateQualityVisibility func() updateQualityVisibility func()
) )
qualityOptions := []string{"Draft (CRF 28)", "Standard (CRF 23)", "High (CRF 18)", "Lossless"} qualityOptions := []string{
"Draft (CRF 28)",
"Standard (CRF 23)",
"Balanced (CRF 20)",
"High (CRF 18)",
"Near-Lossless (CRF 16)",
"Lossless",
}
var syncingQuality bool var syncingQuality bool
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) { qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) {
@ -3792,6 +3799,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
syncingQuality = false syncingQuality = false
}) })
if !slices.Contains(qualityOptions, state.convert.Quality) {
state.convert.Quality = "Standard (CRF 23)"
}
qualitySelectSimple.SetSelected(state.convert.Quality) qualitySelectSimple.SetSelected(state.convert.Quality)
qualitySelectAdv.SetSelected(state.convert.Quality) qualitySelectAdv.SetSelected(state.convert.Quality)
@ -3881,16 +3891,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Show results dialog // Show results dialog
resultText := fmt.Sprintf( resultText := fmt.Sprintf(
"Status: %s\n"+ "Status: %s\n"+
"Interlaced Frames: %.1f%%\n"+ "Interlaced Frames: %.1f%%\n"+
"Field Order: %s\n"+ "Field Order: %s\n"+
"Confidence: %s\n\n"+ "Confidence: %s\n\n"+
"Recommendation:\n%s\n\n"+ "Recommendation:\n%s\n\n"+
"Frame Counts:\n"+ "Frame Counts:\n"+
"Progressive: %d\n"+ "Progressive: %d\n"+
"Top Field First: %d\n"+ "Top Field First: %d\n"+
"Bottom Field First: %d\n"+ "Bottom Field First: %d\n"+
"Undetermined: %d\n"+ "Undetermined: %d\n"+
"Total Analyzed: %d", "Total Analyzed: %d",
result.Status, result.Status,
result.InterlacedPercent, result.InterlacedPercent,
result.FieldOrder, result.FieldOrder,
@ -4338,7 +4348,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Simple resolution selector (separate widget to avoid double-parent issues) // Simple resolution selector (separate widget to avoid double-parent issues)
resolutionSelectSimple := widget.NewSelect([]string{ resolutionSelectSimple := widget.NewSelect([]string{
"Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K", "Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K", "8K",
"2X (relative)", "4X (relative)",
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)", "NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
}, func(value string) { }, func(value string) {
state.convert.TargetResolution = value state.convert.TargetResolution = value
@ -4522,7 +4533,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
updateEncodingControls() updateEncodingControls()
// Target Resolution (advanced) // Target Resolution (advanced)
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×540)", "PAL (720×576)"}, func(value string) { resolutionSelect := widget.NewSelect([]string{
"Source", "720p", "1080p", "1440p", "4K", "8K",
"2X (relative)", "4X (relative)",
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
}, func(value string) {
state.convert.TargetResolution = value state.convert.TargetResolution = value
logging.Debug(logging.CatUI, "target resolution set to %s", value) logging.Debug(logging.CatUI, "target resolution set to %s", value)
}) })
@ -7121,10 +7136,14 @@ func (s *appState) prevVideo() {
func crfForQuality(q string) string { func crfForQuality(q string) string {
switch q { switch q {
case "Balanced (CRF 20)":
return "20"
case "Draft (CRF 28)": case "Draft (CRF 28)":
return "28" return "28"
case "High (CRF 18)": case "High (CRF 18)":
return "18" return "18"
case "Near-Lossless (CRF 16)":
return "16"
case "Lossless": case "Lossless":
return "0" return "0"
default: default:
@ -7478,6 +7497,12 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
// Scaling/Resolution // Scaling/Resolution
if cfg.TargetResolution != "" && cfg.TargetResolution != "Source" { if cfg.TargetResolution != "" && cfg.TargetResolution != "Source" {
var scaleFilter string var scaleFilter string
makeEven := func(v int) int {
if v%2 != 0 {
return v + 1
}
return v
}
switch cfg.TargetResolution { switch cfg.TargetResolution {
case "720p": case "720p":
scaleFilter = "scale=-2:720" scaleFilter = "scale=-2:720"
@ -7495,6 +7520,18 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
scaleFilter = "scale=720:540" scaleFilter = "scale=720:540"
case "PAL (720×576)": case "PAL (720×576)":
scaleFilter = "scale=720:576" scaleFilter = "scale=720:576"
case "2X (relative)":
if src != nil {
w := makeEven(src.Width * 2)
h := makeEven(src.Height * 2)
scaleFilter = fmt.Sprintf("scale=%d:%d", w, h)
}
case "4X (relative)":
if src != nil {
w := makeEven(src.Width * 4)
h := makeEven(src.Height * 4)
scaleFilter = fmt.Sprintf("scale=%d:%d", w, h)
}
} }
if scaleFilter != "" { if scaleFilter != "" {
vf = append(vf, scaleFilter) vf = append(vf, scaleFilter)