|
|
|
@ -183,6 +183,25 @@ func defaultBitrate(codec string, width int, sourceBitrate int) string {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// effectiveHardwareAccel resolves "auto" to a best-effort hardware encoder for the platform.
|
|
|
|
|
|
|
|
func effectiveHardwareAccel(cfg convertConfig) string {
|
|
|
|
|
|
|
|
accel := strings.ToLower(cfg.HardwareAccel)
|
|
|
|
|
|
|
|
if accel != "" && accel != "auto" {
|
|
|
|
|
|
|
|
return accel
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch runtime.GOOS {
|
|
|
|
|
|
|
|
case "windows":
|
|
|
|
|
|
|
|
// Prefer NVENC, then Intel (QSV), then AMD (AMF)
|
|
|
|
|
|
|
|
return "nvenc"
|
|
|
|
|
|
|
|
case "darwin":
|
|
|
|
|
|
|
|
return "videotoolbox"
|
|
|
|
|
|
|
|
default: // linux and others
|
|
|
|
|
|
|
|
// Prefer NVENC, then Intel (QSV), then VAAPI
|
|
|
|
|
|
|
|
return "nvenc"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// openLogViewer opens a simple dialog showing the log content. If live is true, it auto-refreshes.
|
|
|
|
// 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) {
|
|
|
|
func (s *appState) openLogViewer(title, path string, live bool) {
|
|
|
|
if strings.TrimSpace(path) == "" {
|
|
|
|
if strings.TrimSpace(path) == "" {
|
|
|
|
@ -319,7 +338,7 @@ type convertConfig struct {
|
|
|
|
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
|
|
|
|
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
|
|
|
|
FrameRate string // Source, 24, 30, 60, or custom
|
|
|
|
FrameRate string // Source, 24, 30, 60, or custom
|
|
|
|
PixelFormat string // yuv420p, yuv422p, yuv444p
|
|
|
|
PixelFormat string // yuv420p, yuv422p, yuv444p
|
|
|
|
HardwareAccel string // none, nvenc, amf, vaapi, qsv, videotoolbox
|
|
|
|
HardwareAccel string // auto, none, nvenc, amf, vaapi, qsv, videotoolbox
|
|
|
|
TwoPass bool // Enable two-pass encoding for VBR
|
|
|
|
TwoPass bool // Enable two-pass encoding for VBR
|
|
|
|
H264Profile string // baseline, main, high (for H.264 compatibility)
|
|
|
|
H264Profile string // baseline, main, high (for H.264 compatibility)
|
|
|
|
H264Level string // 3.0, 3.1, 4.0, 4.1, 5.0, 5.1 (for H.264 compatibility)
|
|
|
|
H264Level string // 3.0, 3.1, 4.0, 4.1, 5.0, 5.1 (for H.264 compatibility)
|
|
|
|
@ -1334,10 +1353,6 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|
|
|
cfg := job.Config
|
|
|
|
cfg := job.Config
|
|
|
|
inputPath := cfg["inputPath"].(string)
|
|
|
|
inputPath := cfg["inputPath"].(string)
|
|
|
|
outputPath := cfg["outputPath"].(string)
|
|
|
|
outputPath := cfg["outputPath"].(string)
|
|
|
|
sourceBitrate := 0
|
|
|
|
|
|
|
|
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
|
|
|
|
|
|
|
sourceBitrate = int(v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If a direct conversion is running, wait until it finishes before starting queued jobs.
|
|
|
|
// If a direct conversion is running, wait until it finishes before starting queued jobs.
|
|
|
|
for s.convertBusy {
|
|
|
|
for s.convertBusy {
|
|
|
|
@ -1517,7 +1532,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|
|
|
sourceWidth, _ := cfg["sourceWidth"].(int)
|
|
|
|
sourceWidth, _ := cfg["sourceWidth"].(int)
|
|
|
|
sourceHeight, _ := cfg["sourceHeight"].(int)
|
|
|
|
sourceHeight, _ := cfg["sourceHeight"].(int)
|
|
|
|
// Get source bitrate if present
|
|
|
|
// Get source bitrate if present
|
|
|
|
sourceBitrate = 0
|
|
|
|
sourceBitrate := 0
|
|
|
|
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
|
|
|
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
|
|
|
sourceBitrate = int(v)
|
|
|
|
sourceBitrate = int(v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1617,6 +1632,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|
|
|
} else if bitrateMode == "CBR" {
|
|
|
|
} else if bitrateMode == "CBR" {
|
|
|
|
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
|
|
|
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
|
|
|
args = append(args, "-b:v", videoBitrate, "-minrate", videoBitrate, "-maxrate", videoBitrate, "-bufsize", videoBitrate)
|
|
|
|
args = append(args, "-b:v", videoBitrate, "-minrate", videoBitrate, "-maxrate", videoBitrate, "-bufsize", videoBitrate)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
vb := defaultBitrate(videoCodec, sourceWidth, sourceBitrate)
|
|
|
|
|
|
|
|
args = append(args, "-b:v", vb, "-minrate", vb, "-maxrate", vb, "-bufsize", vb)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if bitrateMode == "VBR" {
|
|
|
|
} else if bitrateMode == "VBR" {
|
|
|
|
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
|
|
|
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
|
|
|
@ -2073,7 +2091,7 @@ func runGUI() {
|
|
|
|
TargetResolution: "Source",
|
|
|
|
TargetResolution: "Source",
|
|
|
|
FrameRate: "Source",
|
|
|
|
FrameRate: "Source",
|
|
|
|
PixelFormat: "yuv420p",
|
|
|
|
PixelFormat: "yuv420p",
|
|
|
|
HardwareAccel: "none",
|
|
|
|
HardwareAccel: "auto",
|
|
|
|
TwoPass: false,
|
|
|
|
TwoPass: false,
|
|
|
|
H264Profile: "main",
|
|
|
|
H264Profile: "main",
|
|
|
|
H264Level: "4.0",
|
|
|
|
H264Level: "4.0",
|
|
|
|
@ -2746,7 +2764,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
TargetResolution: "Source",
|
|
|
|
TargetResolution: "Source",
|
|
|
|
FrameRate: "Source",
|
|
|
|
FrameRate: "Source",
|
|
|
|
PixelFormat: "yuv420p",
|
|
|
|
PixelFormat: "yuv420p",
|
|
|
|
HardwareAccel: "none",
|
|
|
|
HardwareAccel: "auto",
|
|
|
|
AudioCodec: "AAC",
|
|
|
|
AudioCodec: "AAC",
|
|
|
|
AudioBitrate: "192k",
|
|
|
|
AudioBitrate: "192k",
|
|
|
|
AudioChannels: "Source",
|
|
|
|
AudioChannels: "Source",
|
|
|
|
@ -2865,6 +2883,30 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
|
|
|
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Simple bitrate selector (shares presets)
|
|
|
|
|
|
|
|
simpleBitrateSelect := widget.NewSelect(bitratePresetLabels, func(value string) {
|
|
|
|
|
|
|
|
state.convert.BitratePreset = value
|
|
|
|
|
|
|
|
if applyBitratePreset != nil {
|
|
|
|
|
|
|
|
applyBitratePreset(value)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Simple resolution selector (separate widget to avoid double-parent issues)
|
|
|
|
|
|
|
|
resolutionSelectSimple := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
|
|
|
|
|
|
|
|
state.convert.TargetResolution = value
|
|
|
|
|
|
|
|
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Simple aspect selector (separate widget)
|
|
|
|
|
|
|
|
targetAspectSelectSimple := widget.NewSelect(aspectTargets, func(value string) {
|
|
|
|
|
|
|
|
logging.Debug(logging.CatUI, "target aspect set to %s (simple)", value)
|
|
|
|
|
|
|
|
state.convert.OutputAspect = value
|
|
|
|
|
|
|
|
updateAspectBoxVisibility()
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
|
|
|
|
|
|
|
|
|
|
|
|
// Target File Size with smart presets + manual entry
|
|
|
|
// Target File Size with smart presets + manual entry
|
|
|
|
targetFileSizeEntry = widget.NewEntry()
|
|
|
|
targetFileSizeEntry = widget.NewEntry()
|
|
|
|
targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB")
|
|
|
|
targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB")
|
|
|
|
@ -3028,11 +3070,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateEncodingControls()
|
|
|
|
updateEncodingControls()
|
|
|
|
|
|
|
|
|
|
|
|
// Target Resolution
|
|
|
|
// Target Resolution (advanced)
|
|
|
|
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
|
|
|
|
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
if state.convert.TargetResolution == "" {
|
|
|
|
|
|
|
|
state.convert.TargetResolution = "Source"
|
|
|
|
|
|
|
|
}
|
|
|
|
resolutionSelect.SetSelected(state.convert.TargetResolution)
|
|
|
|
resolutionSelect.SetSelected(state.convert.TargetResolution)
|
|
|
|
|
|
|
|
|
|
|
|
// Frame Rate with hint
|
|
|
|
// Frame Rate with hint
|
|
|
|
@ -3110,11 +3155,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
})
|
|
|
|
})
|
|
|
|
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
|
|
|
|
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
|
|
|
|
|
|
|
|
|
|
|
|
// Hardware Acceleration
|
|
|
|
// Hardware Acceleration with hint
|
|
|
|
hwAccelSelect := widget.NewSelect([]string{"none", "nvenc", "amf", "vaapi", "qsv", "videotoolbox"}, func(value string) {
|
|
|
|
hwAccelHint := widget.NewLabel("Auto picks the best GPU path; if encode fails, switch to none (software).")
|
|
|
|
|
|
|
|
hwAccelHint.Wrapping = fyne.TextWrapWord
|
|
|
|
|
|
|
|
hwAccelSelect := widget.NewSelect([]string{"auto", "none", "nvenc", "amf", "vaapi", "qsv", "videotoolbox"}, func(value string) {
|
|
|
|
state.convert.HardwareAccel = value
|
|
|
|
state.convert.HardwareAccel = value
|
|
|
|
logging.Debug(logging.CatUI, "hardware accel set to %s", value)
|
|
|
|
logging.Debug(logging.CatUI, "hardware accel set to %s", value)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
if state.convert.HardwareAccel == "" {
|
|
|
|
|
|
|
|
state.convert.HardwareAccel = "auto"
|
|
|
|
|
|
|
|
}
|
|
|
|
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
|
|
|
|
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
|
|
|
|
|
|
|
|
|
|
|
|
// Two-Pass encoding
|
|
|
|
// Two-Pass encoding
|
|
|
|
@ -3217,7 +3267,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
widget.NewLabel("Choose slower for better compression, faster for speed"),
|
|
|
|
widget.NewLabel("Choose slower for better compression, faster for speed"),
|
|
|
|
widget.NewLabelWithStyle("Encoder Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
widget.NewLabelWithStyle("Encoder Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
simplePresetSelect,
|
|
|
|
simplePresetSelect,
|
|
|
|
widget.NewLabel("Aspect ratio will match source video"),
|
|
|
|
widget.NewSeparator(),
|
|
|
|
|
|
|
|
widget.NewLabelWithStyle("Bitrate (simple presets)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
|
|
|
|
simpleBitrateSelect,
|
|
|
|
|
|
|
|
widget.NewLabelWithStyle("Target Resolution", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
|
|
|
|
resolutionSelectSimple,
|
|
|
|
|
|
|
|
widget.NewLabelWithStyle("Target Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
|
|
|
|
targetAspectSelectSimple,
|
|
|
|
|
|
|
|
targetAspectHint,
|
|
|
|
layout.NewSpacer(),
|
|
|
|
layout.NewSpacer(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@ -3261,6 +3318,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
pixelFormatSelect,
|
|
|
|
pixelFormatSelect,
|
|
|
|
widget.NewLabelWithStyle("Hardware Acceleration", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
widget.NewLabelWithStyle("Hardware Acceleration", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
|
|
hwAccelSelect,
|
|
|
|
hwAccelSelect,
|
|
|
|
|
|
|
|
hwAccelHint,
|
|
|
|
twoPassCheck,
|
|
|
|
twoPassCheck,
|
|
|
|
widget.NewSeparator(),
|
|
|
|
widget.NewSeparator(),
|
|
|
|
|
|
|
|
|
|
|
|
@ -3328,23 +3386,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|
|
|
tabs.OnSelected = func(item *container.TabItem) {
|
|
|
|
tabs.OnSelected = func(item *container.TabItem) {
|
|
|
|
if item.Text == "Simple" {
|
|
|
|
if item.Text == "Simple" {
|
|
|
|
state.convert.Mode = "Simple"
|
|
|
|
state.convert.Mode = "Simple"
|
|
|
|
// Lock aspect ratio to Source in Simple mode
|
|
|
|
logging.Debug(logging.CatUI, "convert mode selected: Simple")
|
|
|
|
state.convert.OutputAspect = "Source"
|
|
|
|
|
|
|
|
targetAspectSelect.SetSelected("Source")
|
|
|
|
|
|
|
|
updateAspectBoxVisibility()
|
|
|
|
|
|
|
|
logging.Debug(logging.CatUI, "convert mode selected: Simple (aspect locked to Source)")
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
state.convert.Mode = "Advanced"
|
|
|
|
state.convert.Mode = "Advanced"
|
|
|
|
logging.Debug(logging.CatUI, "convert mode selected: Advanced")
|
|
|
|
logging.Debug(logging.CatUI, "convert mode selected: Advanced")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure Simple mode starts with Source aspect
|
|
|
|
|
|
|
|
if state.convert.Mode == "Simple" {
|
|
|
|
|
|
|
|
state.convert.OutputAspect = "Source"
|
|
|
|
|
|
|
|
targetAspectSelect.SetSelected("Source")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optionsRect := canvas.NewRectangle(utils.MustHex("#13182B"))
|
|
|
|
optionsRect := canvas.NewRectangle(utils.MustHex("#13182B"))
|
|
|
|
optionsRect.CornerRadius = 8
|
|
|
|
optionsRect.CornerRadius = 8
|
|
|
|
optionsRect.StrokeColor = gridColor
|
|
|
|
optionsRect.StrokeColor = gridColor
|
|
|
|
@ -5191,27 +5239,28 @@ func detectBestH265Encoder() string {
|
|
|
|
|
|
|
|
|
|
|
|
// determineVideoCodec maps user-friendly codec names to FFmpeg codec names
|
|
|
|
// determineVideoCodec maps user-friendly codec names to FFmpeg codec names
|
|
|
|
func determineVideoCodec(cfg convertConfig) string {
|
|
|
|
func determineVideoCodec(cfg convertConfig) string {
|
|
|
|
|
|
|
|
accel := effectiveHardwareAccel(cfg)
|
|
|
|
switch cfg.VideoCodec {
|
|
|
|
switch cfg.VideoCodec {
|
|
|
|
case "H.264":
|
|
|
|
case "H.264":
|
|
|
|
if cfg.HardwareAccel == "nvenc" {
|
|
|
|
if accel == "nvenc" {
|
|
|
|
return "h264_nvenc"
|
|
|
|
return "h264_nvenc"
|
|
|
|
} else if cfg.HardwareAccel == "amf" {
|
|
|
|
} else if accel == "amf" {
|
|
|
|
return "h264_amf"
|
|
|
|
return "h264_amf"
|
|
|
|
} else if cfg.HardwareAccel == "qsv" {
|
|
|
|
} else if accel == "qsv" {
|
|
|
|
return "h264_qsv"
|
|
|
|
return "h264_qsv"
|
|
|
|
} else if cfg.HardwareAccel == "videotoolbox" {
|
|
|
|
} else if accel == "videotoolbox" {
|
|
|
|
return "h264_videotoolbox"
|
|
|
|
return "h264_videotoolbox"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
return "libx264"
|
|
|
|
return "libx264"
|
|
|
|
case "H.265":
|
|
|
|
case "H.265":
|
|
|
|
if cfg.HardwareAccel == "nvenc" {
|
|
|
|
if accel == "nvenc" {
|
|
|
|
return "hevc_nvenc"
|
|
|
|
return "hevc_nvenc"
|
|
|
|
} else if cfg.HardwareAccel == "amf" {
|
|
|
|
} else if accel == "amf" {
|
|
|
|
return "hevc_amf"
|
|
|
|
return "hevc_amf"
|
|
|
|
} else if cfg.HardwareAccel == "qsv" {
|
|
|
|
} else if accel == "qsv" {
|
|
|
|
return "hevc_qsv"
|
|
|
|
return "hevc_qsv"
|
|
|
|
} else if cfg.HardwareAccel == "videotoolbox" {
|
|
|
|
} else if accel == "videotoolbox" {
|
|
|
|
return "hevc_videotoolbox"
|
|
|
|
return "hevc_videotoolbox"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
@ -5219,13 +5268,13 @@ func determineVideoCodec(cfg convertConfig) string {
|
|
|
|
case "VP9":
|
|
|
|
case "VP9":
|
|
|
|
return "libvpx-vp9"
|
|
|
|
return "libvpx-vp9"
|
|
|
|
case "AV1":
|
|
|
|
case "AV1":
|
|
|
|
if cfg.HardwareAccel == "amf" {
|
|
|
|
if accel == "amf" {
|
|
|
|
return "av1_amf"
|
|
|
|
return "av1_amf"
|
|
|
|
} else if cfg.HardwareAccel == "nvenc" {
|
|
|
|
} else if accel == "nvenc" {
|
|
|
|
return "av1_nvenc"
|
|
|
|
return "av1_nvenc"
|
|
|
|
} else if cfg.HardwareAccel == "qsv" {
|
|
|
|
} else if accel == "qsv" {
|
|
|
|
return "av1_qsv"
|
|
|
|
return "av1_qsv"
|
|
|
|
} else if cfg.HardwareAccel == "vaapi" {
|
|
|
|
} else if accel == "vaapi" {
|
|
|
|
return "av1_vaapi"
|
|
|
|
return "av1_vaapi"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
// When set to "none" or empty, use software encoder
|
|
|
|
@ -5307,6 +5356,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
|
|
|
}
|
|
|
|
}
|
|
|
|
src := s.source
|
|
|
|
src := s.source
|
|
|
|
cfg := s.convert
|
|
|
|
cfg := s.convert
|
|
|
|
|
|
|
|
sourceBitrate := src.Bitrate
|
|
|
|
isDVD := cfg.SelectedFormat.Ext == ".mpg"
|
|
|
|
isDVD := cfg.SelectedFormat.Ext == ".mpg"
|
|
|
|
outDir := filepath.Dir(src.Path)
|
|
|
|
outDir := filepath.Dir(src.Path)
|
|
|
|
outName := cfg.OutputFile()
|
|
|
|
outName := cfg.OutputFile()
|
|
|
|
@ -5358,16 +5408,13 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
|
|
|
args = append(args, "-i", cfg.CoverArtPath)
|
|
|
|
args = append(args, "-i", cfg.CoverArtPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hardware acceleration for decoding
|
|
|
|
// Hardware acceleration for decoding (best-effort)
|
|
|
|
// Note: NVENC and AMF don't need -hwaccel for encoding, only for decoding
|
|
|
|
if accel := effectiveHardwareAccel(cfg); accel != "none" && accel != "" {
|
|
|
|
if cfg.HardwareAccel != "none" && cfg.HardwareAccel != "" {
|
|
|
|
switch accel {
|
|
|
|
switch cfg.HardwareAccel {
|
|
|
|
|
|
|
|
case "nvenc":
|
|
|
|
case "nvenc":
|
|
|
|
// For NVENC, we don't add -hwaccel flags
|
|
|
|
// NVENC encoders handle GPU directly; no hwaccel flag needed
|
|
|
|
// The h264_nvenc/hevc_nvenc encoder handles GPU encoding directly
|
|
|
|
|
|
|
|
case "amf":
|
|
|
|
case "amf":
|
|
|
|
// For AMD AMF, we don't add -hwaccel flags
|
|
|
|
// AMF encoders handle GPU directly
|
|
|
|
// The h264_amf/hevc_amf/av1_amf encoders handle GPU encoding directly
|
|
|
|
|
|
|
|
case "vaapi":
|
|
|
|
case "vaapi":
|
|
|
|
args = append(args, "-hwaccel", "vaapi")
|
|
|
|
args = append(args, "-hwaccel", "vaapi")
|
|
|
|
case "qsv":
|
|
|
|
case "qsv":
|
|
|
|
@ -5375,7 +5422,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
|
|
|
case "videotoolbox":
|
|
|
|
case "videotoolbox":
|
|
|
|
args = append(args, "-hwaccel", "videotoolbox")
|
|
|
|
args = append(args, "-hwaccel", "videotoolbox")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logging.Debug(logging.CatFFMPEG, "hardware acceleration: %s", cfg.HardwareAccel)
|
|
|
|
logging.Debug(logging.CatFFMPEG, "hardware acceleration: %s", accel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Video filters.
|
|
|
|
// Video filters.
|
|
|
|
@ -6699,30 +6746,27 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
// File Info section
|
|
|
|
// File Info section
|
|
|
|
comparisonText.WriteString("━━━ FILE INFO ━━━\n")
|
|
|
|
comparisonText.WriteString("━━━ FILE INFO ━━━\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var file1SizeBytes int64
|
|
|
|
file1Size := getField(state.compareFile1, func(src *videoSource) string {
|
|
|
|
file1Size := getField(state.compareFile1, func(src *videoSource) string {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
|
|
file1SizeBytes = fi.Size()
|
|
|
|
if sizeMB >= 1024 {
|
|
|
|
return utils.FormatBytes(fi.Size())
|
|
|
|
return fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%.2f MB", sizeMB)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "Unknown"
|
|
|
|
return "Unknown"
|
|
|
|
})
|
|
|
|
})
|
|
|
|
file2Size := getField(state.compareFile2, func(src *videoSource) string {
|
|
|
|
file2Size := getField(state.compareFile2, func(src *videoSource) string {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
|
|
if file1SizeBytes > 0 {
|
|
|
|
if sizeMB >= 1024 {
|
|
|
|
return utils.DeltaBytes(fi.Size(), file1SizeBytes)
|
|
|
|
return fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%.2f MB", sizeMB)
|
|
|
|
return utils.FormatBytes(fi.Size())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "Unknown"
|
|
|
|
return "Unknown"
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n", "File Size:", file1Size, file2Size))
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n", "File Size:", file1Size, file2Size))
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
"Format:",
|
|
|
|
"Format Family:",
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return s.Format }),
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return s.Format }),
|
|
|
|
getField(state.compareFile2, func(s *videoSource) string { return s.Format })))
|
|
|
|
getField(state.compareFile2, func(s *videoSource) string { return s.Format })))
|
|
|
|
|
|
|
|
|
|
|
|
@ -6747,7 +6791,12 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
"Bitrate:",
|
|
|
|
"Bitrate:",
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return formatBitrate(s.Bitrate) }),
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return formatBitrate(s.Bitrate) }),
|
|
|
|
getField(state.compareFile2, func(s *videoSource) string { return formatBitrate(s.Bitrate) })))
|
|
|
|
getField(state.compareFile2, func(s *videoSource) string {
|
|
|
|
|
|
|
|
if state.compareFile1 != nil {
|
|
|
|
|
|
|
|
return utils.DeltaBitrate(s.Bitrate, state.compareFile1.Bitrate)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return formatBitrate(s.Bitrate)
|
|
|
|
|
|
|
|
})))
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
|
|
|
"Pixel Format:",
|
|
|
|
"Pixel Format:",
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return s.PixelFormat }),
|
|
|
|
getField(state.compareFile1, func(s *videoSource) string { return s.PixelFormat }),
|
|
|
|
@ -6848,22 +6897,38 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
file2Info.Wrapping = fyne.TextWrapWord
|
|
|
|
file2Info.Wrapping = fyne.TextWrapWord
|
|
|
|
file2Info.TextStyle = fyne.TextStyle{} // non-selectable label
|
|
|
|
file2Info.TextStyle = fyne.TextStyle{} // non-selectable label
|
|
|
|
|
|
|
|
|
|
|
|
// Helper function to format metadata
|
|
|
|
// Helper function to format metadata (optionally comparing to a reference video)
|
|
|
|
formatMetadata := func(src *videoSource) string {
|
|
|
|
formatMetadata := func(src *videoSource, ref *videoSource) string {
|
|
|
|
fileSize := "Unknown"
|
|
|
|
var (
|
|
|
|
|
|
|
|
fileSize = "Unknown"
|
|
|
|
|
|
|
|
refSize int64 = 0
|
|
|
|
|
|
|
|
)
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
|
|
if ref != nil {
|
|
|
|
if sizeMB >= 1024 {
|
|
|
|
if rfi, err := os.Stat(ref.Path); err == nil {
|
|
|
|
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
|
|
refSize = rfi.Size()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if refSize > 0 {
|
|
|
|
|
|
|
|
fileSize = utils.DeltaBytes(fi.Size(), refSize)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
|
|
|
|
fileSize = utils.FormatBytes(fi.Size())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var bitrateStr string
|
|
|
|
|
|
|
|
if src.Bitrate > 0 {
|
|
|
|
var (
|
|
|
|
bitrateStr = formatBitrate(src.Bitrate)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
bitrateStr = "--"
|
|
|
|
bitrateStr = "--"
|
|
|
|
|
|
|
|
refBitrate = 0
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if ref != nil {
|
|
|
|
|
|
|
|
refBitrate = ref.Bitrate
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if src.Bitrate > 0 {
|
|
|
|
|
|
|
|
if refBitrate > 0 {
|
|
|
|
|
|
|
|
bitrateStr = utils.DeltaBitrate(src.Bitrate, refBitrate)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
bitrateStr = formatBitrate(src.Bitrate)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
return fmt.Sprintf(
|
|
|
|
@ -6944,7 +7009,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
filename := filepath.Base(state.compareFile1.Path)
|
|
|
|
filename := filepath.Base(state.compareFile1.Path)
|
|
|
|
displayName := truncateFilename(filename, 35)
|
|
|
|
displayName := truncateFilename(filename, 35)
|
|
|
|
file1Label.SetText(fmt.Sprintf("File 1: %s", displayName))
|
|
|
|
file1Label.SetText(fmt.Sprintf("File 1: %s", displayName))
|
|
|
|
file1Info.SetText(formatMetadata(state.compareFile1))
|
|
|
|
file1Info.SetText(formatMetadata(state.compareFile1, state.compareFile2))
|
|
|
|
// Build video player with compact size for side-by-side
|
|
|
|
// Build video player with compact size for side-by-side
|
|
|
|
file1VideoContainer.Objects = []fyne.CanvasObject{
|
|
|
|
file1VideoContainer.Objects = []fyne.CanvasObject{
|
|
|
|
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile1, nil),
|
|
|
|
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile1, nil),
|
|
|
|
@ -6965,7 +7030,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
filename := filepath.Base(state.compareFile2.Path)
|
|
|
|
filename := filepath.Base(state.compareFile2.Path)
|
|
|
|
displayName := truncateFilename(filename, 35)
|
|
|
|
displayName := truncateFilename(filename, 35)
|
|
|
|
file2Label.SetText(fmt.Sprintf("File 2: %s", displayName))
|
|
|
|
file2Label.SetText(fmt.Sprintf("File 2: %s", displayName))
|
|
|
|
file2Info.SetText(formatMetadata(state.compareFile2))
|
|
|
|
file2Info.SetText(formatMetadata(state.compareFile2, state.compareFile1))
|
|
|
|
// Build video player with compact size for side-by-side
|
|
|
|
// Build video player with compact size for side-by-side
|
|
|
|
file2VideoContainer.Objects = []fyne.CanvasObject{
|
|
|
|
file2VideoContainer.Objects = []fyne.CanvasObject{
|
|
|
|
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile2, nil),
|
|
|
|
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile2, nil),
|
|
|
|
@ -7030,7 +7095,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
if state.compareFile1 == nil {
|
|
|
|
if state.compareFile1 == nil {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
metadata := formatMetadata(state.compareFile1)
|
|
|
|
metadata := formatMetadata(state.compareFile1, state.compareFile2)
|
|
|
|
state.window.Clipboard().SetContent(metadata)
|
|
|
|
state.window.Clipboard().SetContent(metadata)
|
|
|
|
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
|
|
|
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
@ -7047,7 +7112,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|
|
|
if state.compareFile2 == nil {
|
|
|
|
if state.compareFile2 == nil {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
metadata := formatMetadata(state.compareFile2)
|
|
|
|
metadata := formatMetadata(state.compareFile2, state.compareFile1)
|
|
|
|
state.window.Clipboard().SetContent(metadata)
|
|
|
|
state.window.Clipboard().SetContent(metadata)
|
|
|
|
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
|
|
|
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
@ -7159,19 +7224,14 @@ func buildInspectView(state *appState) fyne.CanvasObject {
|
|
|
|
formatMetadata := func(src *videoSource) string {
|
|
|
|
formatMetadata := func(src *videoSource) string {
|
|
|
|
fileSize := "Unknown"
|
|
|
|
fileSize := "Unknown"
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
if fi, err := os.Stat(src.Path); err == nil {
|
|
|
|
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
|
|
fileSize = utils.FormatBytes(fi.Size())
|
|
|
|
if sizeMB >= 1024 {
|
|
|
|
|
|
|
|
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
return fmt.Sprintf(
|
|
|
|
"━━━ FILE INFO ━━━\n"+
|
|
|
|
"━━━ FILE INFO ━━━\n"+
|
|
|
|
"Path: %s\n"+
|
|
|
|
"Path: %s\n"+
|
|
|
|
"File Size: %s\n"+
|
|
|
|
"File Size: %s\n"+
|
|
|
|
"Format: %s\n"+
|
|
|
|
"Format Family: %s\n"+
|
|
|
|
"\n━━━ VIDEO ━━━\n"+
|
|
|
|
"\n━━━ VIDEO ━━━\n"+
|
|
|
|
"Codec: %s\n"+
|
|
|
|
"Codec: %s\n"+
|
|
|
|
"Resolution: %dx%d\n"+
|
|
|
|
"Resolution: %dx%d\n"+
|
|
|
|
|