@ -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.
func ( s * appState ) openLogViewer ( title , path string , live bool ) {
if strings . TrimSpace ( path ) == "" {
@ -319,7 +338,7 @@ type convertConfig struct {
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
FrameRate string // Source, 24, 30, 60, or custom
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
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)
@ -1334,10 +1353,6 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
cfg := job . Config
inputPath := cfg [ "inputPath" ] . ( 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.
for s . convertBusy {
@ -1517,7 +1532,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
sourceWidth , _ := cfg [ "sourceWidth" ] . ( int )
sourceHeight , _ := cfg [ "sourceHeight" ] . ( int )
// Get source bitrate if present
sourceBitrate = 0
sourceBitrate : = 0
if v , ok := cfg [ "sourceBitrate" ] . ( float64 ) ; ok {
sourceBitrate = int ( v )
}
@ -1617,6 +1632,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
} else if bitrateMode == "CBR" {
if videoBitrate , _ := cfg [ "videoBitrate" ] . ( string ) ; 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" {
if videoBitrate , _ := cfg [ "videoBitrate" ] . ( string ) ; videoBitrate != "" {
@ -2073,7 +2091,7 @@ func runGUI() {
TargetResolution : "Source" ,
FrameRate : "Source" ,
PixelFormat : "yuv420p" ,
HardwareAccel : " none ",
HardwareAccel : " auto ",
TwoPass : false ,
H264Profile : "main" ,
H264Level : "4.0" ,
@ -2746,7 +2764,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
TargetResolution : "Source" ,
FrameRate : "Source" ,
PixelFormat : "yuv420p" ,
HardwareAccel : " none ",
HardwareAccel : " auto ",
AudioCodec : "AAC" ,
AudioBitrate : "192k" ,
AudioChannels : "Source" ,
@ -2865,6 +2883,30 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
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
targetFileSizeEntry = widget . NewEntry ( )
targetFileSizeEntry . SetPlaceHolder ( "e.g., 25MB, 100MB, 8MB" )
@ -3028,11 +3070,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
updateEncodingControls ( )
// Target Resolution
// Target Resolution (advanced)
resolutionSelect := 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" , value )
} )
if state . convert . TargetResolution == "" {
state . convert . TargetResolution = "Source"
}
resolutionSelect . SetSelected ( state . convert . TargetResolution )
// Frame Rate with hint
@ -3110,11 +3155,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} )
pixelFormatSelect . SetSelected ( state . convert . PixelFormat )
// Hardware Acceleration
hwAccelSelect := widget . NewSelect ( [ ] string { "none" , "nvenc" , "amf" , "vaapi" , "qsv" , "videotoolbox" } , func ( value string ) {
// Hardware Acceleration with hint
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
logging . Debug ( logging . CatUI , "hardware accel set to %s" , value )
} )
if state . convert . HardwareAccel == "" {
state . convert . HardwareAccel = "auto"
}
hwAccelSelect . SetSelected ( state . convert . HardwareAccel )
// 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 . NewLabelWithStyle ( "Encoder Preset" , fyne . TextAlignLeading , fyne . TextStyle { Bold : true } ) ,
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 ( ) ,
)
@ -3261,6 +3318,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
pixelFormatSelect ,
widget . NewLabelWithStyle ( "Hardware Acceleration" , fyne . TextAlignLeading , fyne . TextStyle { Bold : true } ) ,
hwAccelSelect ,
hwAccelHint ,
twoPassCheck ,
widget . NewSeparator ( ) ,
@ -3328,23 +3386,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
tabs . OnSelected = func ( item * container . TabItem ) {
if item . Text == "Simple" {
state . convert . Mode = "Simple"
// Lock aspect ratio to Source in Simple mode
state . convert . OutputAspect = "Source"
targetAspectSelect . SetSelected ( "Source" )
updateAspectBoxVisibility ( )
logging . Debug ( logging . CatUI , "convert mode selected: Simple (aspect locked to Source)" )
logging . Debug ( logging . CatUI , "convert mode selected: Simple" )
} else {
state . convert . Mode = "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 . CornerRadius = 8
optionsRect . StrokeColor = gridColor
@ -5191,27 +5239,28 @@ func detectBestH265Encoder() string {
// determineVideoCodec maps user-friendly codec names to FFmpeg codec names
func determineVideoCodec ( cfg convertConfig ) string {
accel := effectiveHardwareAccel ( cfg )
switch cfg . VideoCodec {
case "H.264" :
if cfg. H ardwareA ccel == "nvenc" {
if accel == "nvenc" {
return "h264_nvenc"
} else if cfg. H ardwareA ccel == "amf" {
} else if accel == "amf" {
return "h264_amf"
} else if cfg. H ardwareA ccel == "qsv" {
} else if accel == "qsv" {
return "h264_qsv"
} else if cfg. H ardwareA ccel == "videotoolbox" {
} else if accel == "videotoolbox" {
return "h264_videotoolbox"
}
// When set to "none" or empty, use software encoder
return "libx264"
case "H.265" :
if cfg. H ardwareA ccel == "nvenc" {
if accel == "nvenc" {
return "hevc_nvenc"
} else if cfg. H ardwareA ccel == "amf" {
} else if accel == "amf" {
return "hevc_amf"
} else if cfg. H ardwareA ccel == "qsv" {
} else if accel == "qsv" {
return "hevc_qsv"
} else if cfg. H ardwareA ccel == "videotoolbox" {
} else if accel == "videotoolbox" {
return "hevc_videotoolbox"
}
// When set to "none" or empty, use software encoder
@ -5219,13 +5268,13 @@ func determineVideoCodec(cfg convertConfig) string {
case "VP9" :
return "libvpx-vp9"
case "AV1" :
if cfg. H ardwareA ccel == "amf" {
if accel == "amf" {
return "av1_amf"
} else if cfg. H ardwareA ccel == "nvenc" {
} else if accel == "nvenc" {
return "av1_nvenc"
} else if cfg. H ardwareA ccel == "qsv" {
} else if accel == "qsv" {
return "av1_qsv"
} else if cfg. H ardwareA ccel == "vaapi" {
} else if accel == "vaapi" {
return "av1_vaapi"
}
// 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
cfg := s . convert
sourceBitrate := src . Bitrate
isDVD := cfg . SelectedFormat . Ext == ".mpg"
outDir := filepath . Dir ( src . Path )
outName := cfg . OutputFile ( )
@ -5358,16 +5408,13 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
args = append ( args , "-i" , cfg . CoverArtPath )
}
// Hardware acceleration for decoding
// Note: NVENC and AMF don't need -hwaccel for encoding, only for decoding
if cfg . HardwareAccel != "none" && cfg . HardwareAccel != "" {
switch cfg . HardwareAccel {
// Hardware acceleration for decoding (best-effort)
if accel := effectiveHardwareAccel ( cfg ) ; accel != "none" && accel != "" {
switch accel {
case "nvenc" :
// For NVENC, we don't add -hwaccel flags
// The h264_nvenc/hevc_nvenc encoder handles GPU encoding directly
// NVENC encoders handle GPU directly; no hwaccel flag needed
case "amf" :
// For AMD AMF, we don't add -hwaccel flags
// The h264_amf/hevc_amf/av1_amf encoders handle GPU encoding directly
// AMF encoders handle GPU directly
case "vaapi" :
args = append ( args , "-hwaccel" , "vaapi" )
case "qsv" :
@ -5375,7 +5422,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
case "videotoolbox" :
args = append ( args , "-hwaccel" , "videotoolbox" )
}
logging . Debug ( logging . CatFFMPEG , "hardware acceleration: %s" , cfg. H ardwareA ccel)
logging . Debug ( logging . CatFFMPEG , "hardware acceleration: %s" , accel)
}
// Video filters.
@ -6699,30 +6746,27 @@ func buildCompareView(state *appState) fyne.CanvasObject {
// File Info section
comparisonText . WriteString ( "━━━ FILE INFO ━━━\n" )
var file1SizeBytes int64
file1Size := getField ( state . compareFile1 , func ( src * videoSource ) string {
if fi , err := os . Stat ( src . Path ) ; err == nil {
sizeMB := float64 ( fi . Size ( ) ) / ( 1024 * 1024 )
if sizeMB >= 1024 {
return fmt . Sprintf ( "%.2f GB" , sizeMB / 1024 )
}
return fmt . Sprintf ( "%.2f MB" , sizeMB )
file1SizeBytes = fi . Size ( )
return utils . FormatBytes ( fi . Size ( ) )
}
return "Unknown"
} )
file2Size := getField ( state . compareFile2 , func ( src * videoSource ) string {
if fi , err := os . Stat ( src . Path ) ; err == nil {
sizeMB := float64 ( fi . Size ( ) ) / ( 1024 * 1024 )
if sizeMB >= 1024 {
return fmt . Sprintf ( "%.2f GB" , sizeMB / 1024 )
if file1SizeBytes > 0 {
return utils . DeltaBytes ( fi . Size ( ) , file1SizeBytes )
}
return fmt. Sprintf ( "%.2f MB" , sizeMB )
return utils. FormatBytes ( fi . Size ( ) )
}
return "Unknown"
} )
comparisonText . WriteString ( fmt . Sprintf ( "%-25s | %-20s | %s\n" , "File Size:" , file1Size , file2Size ) )
comparisonText . WriteString ( fmt . Sprintf ( "%-25s | %-20s | %s\n" ,
"Format :",
"Format Family :",
getField ( state . compareFile1 , 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" ,
"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" ,
"Pixel Format:" ,
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 . TextStyle = fyne . TextStyle { } // non-selectable label
// Helper function to format metadata
formatMetadata := func ( src * videoSource ) string {
fileSize := "Unknown"
// Helper function to format metadata (optionally comparing to a reference video)
formatMetadata := func ( src * videoSource , ref * videoSource ) string {
var (
fileSize = "Unknown"
refSize int64 = 0
)
if fi , err := os . Stat ( src . Path ) ; err == nil {
sizeMB := float64 ( fi . Size ( ) ) / ( 1024 * 1024 )
if sizeMB >= 1024 {
fileSize = fmt . Sprintf ( "%.2f GB" , sizeMB / 1024 )
if ref != nil {
if rfi , err := os . Stat ( ref . Path ) ; err == nil {
refSize = rfi . Size ( )
}
}
if refSize > 0 {
fileSize = utils . DeltaBytes ( fi . Size ( ) , refSize )
} else {
fileSize = fmt . Sprintf ( "%.2f MB" , sizeMB )
fileSize = utils. FormatBytes ( fi . Size ( ) )
}
}
var bitrateStr string
if src . Bitrate > 0 {
bitrateStr = formatBitrate ( src . Bitrate )
} else {
var (
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 (
@ -6944,7 +7009,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
filename := filepath . Base ( state . compareFile1 . Path )
displayName := truncateFilename ( filename , 35 )
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
file1VideoContainer . Objects = [ ] fyne . CanvasObject {
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 )
displayName := truncateFilename ( filename , 35 )
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
file2VideoContainer . Objects = [ ] fyne . CanvasObject {
buildVideoPane ( state , fyne . NewSize ( 320 , 180 ) , state . compareFile2 , nil ) ,
@ -7030,7 +7095,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
if state . compareFile1 == nil {
return
}
metadata := formatMetadata ( state . compareFile1 )
metadata := formatMetadata ( state . compareFile1 , state . compareFile2 )
state . window . Clipboard ( ) . SetContent ( metadata )
dialog . ShowInformation ( "Copied" , "Metadata copied to clipboard" , state . window )
} )
@ -7047,7 +7112,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
if state . compareFile2 == nil {
return
}
metadata := formatMetadata ( state . compareFile2 )
metadata := formatMetadata ( state . compareFile2 , state . compareFile1 )
state . window . Clipboard ( ) . SetContent ( metadata )
dialog . ShowInformation ( "Copied" , "Metadata copied to clipboard" , state . window )
} )
@ -7159,19 +7224,14 @@ func buildInspectView(state *appState) fyne.CanvasObject {
formatMetadata := func ( src * videoSource ) string {
fileSize := "Unknown"
if fi , err := os . Stat ( src . Path ) ; err == nil {
sizeMB := float64 ( fi . Size ( ) ) / ( 1024 * 1024 )
if sizeMB >= 1024 {
fileSize = fmt . Sprintf ( "%.2f GB" , sizeMB / 1024 )
} else {
fileSize = fmt . Sprintf ( "%.2f MB" , sizeMB )
}
fileSize = utils . FormatBytes ( fi . Size ( ) )
}
return fmt . Sprintf (
"━━━ FILE INFO ━━━\n" +
"Path: %s\n" +
"File Size: %s\n" +
"Format : %s\n"+
"Format Family : %s\n"+
"\n━━━ VIDEO ━━━\n" +
"Codec: %s\n" +
"Resolution: %dx%d\n" +