diff --git a/internal/convert/ffmpeg.go b/internal/convert/ffmpeg.go index ea4736f..d150a68 100644 --- a/internal/convert/ffmpeg.go +++ b/internal/convert/ffmpeg.go @@ -98,6 +98,7 @@ func ProbeVideo(path string) (*VideoSource, error) { "-show_streams", path, ) + utils.ApplyNoWindow(cmd) out, err := cmd.Output() if err != nil { return nil, err @@ -155,6 +156,13 @@ func ProbeVideo(path string) (*VideoSource, error) { } } + if len(result.Format.Tags) > 0 { + src.Metadata = normalizeTags(result.Format.Tags) + if len(src.Metadata) > 0 { + src.HasMetadata = true + } + } + // Check for chapters src.HasChapters = len(result.Chapters) > 0 @@ -252,6 +260,7 @@ func ProbeVideo(path string) (*VideoSource, error) { "-y", coverPath, ) + utils.ApplyNoWindow(extractCmd) if err := extractCmd.Run(); err != nil { logging.Debug(logging.CatFFMPEG, "failed to extract embedded cover art: %v", err) } else { @@ -271,6 +280,21 @@ func ProbeVideo(path string) (*VideoSource, error) { return src, nil } +func normalizeTags(tags map[string]interface{}) map[string]string { + normalized := make(map[string]string, len(tags)) + for k, v := range tags { + key := strings.ToLower(strings.TrimSpace(k)) + if key == "" { + continue + } + val := strings.TrimSpace(fmt.Sprint(v)) + if val != "" { + normalized[key] = val + } + } + return normalized +} + // detectGOPSize attempts to detect GOP size by examining key frames func detectGOPSize(ctx context.Context, path string) int { // Use ffprobe to show frames and look for key_frame markers @@ -283,6 +307,7 @@ func detectGOPSize(ctx context.Context, path string) int { "-print_format", "json", path, ) + utils.ApplyNoWindow(cmd) out, err := cmd.Output() if err != nil { diff --git a/internal/utils/proc_other.go b/internal/utils/proc_other.go new file mode 100644 index 0000000..56f7aae --- /dev/null +++ b/internal/utils/proc_other.go @@ -0,0 +1,10 @@ +//go:build !windows + +package utils + +import "os/exec" + +// ApplyNoWindow is a no-op on non-Windows platforms. +func ApplyNoWindow(cmd *exec.Cmd) { + _ = cmd +} diff --git a/internal/utils/proc_windows.go b/internal/utils/proc_windows.go new file mode 100644 index 0000000..a018683 --- /dev/null +++ b/internal/utils/proc_windows.go @@ -0,0 +1,16 @@ +//go:build windows + +package utils + +import ( + "os/exec" + "syscall" +) + +// ApplyNoWindow hides the console window for spawned processes on Windows. +func ApplyNoWindow(cmd *exec.Cmd) { + if cmd == nil { + return + } + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} +} diff --git a/main.go b/main.go index 42806e4..eb55245 100644 --- a/main.go +++ b/main.go @@ -128,6 +128,7 @@ type convertConfig struct { EncoderPreset string // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow CRF string // Manual CRF value (0-51, or empty to use Quality preset) BitrateMode string // CRF, CBR, VBR, "Target Size" + BitratePreset string // Friendly bitrate presets (codec-aware recommendations) VideoBitrate string // For CBR/VBR modes (e.g., "5000k") TargetFileSize string // Target file size (e.g., "25MB", "100MB") - requires BitrateMode="Target Size" TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom @@ -682,6 +683,7 @@ func (s *appState) addConvertToQueue() error { "encoderPreset": cfg.EncoderPreset, "crf": cfg.CRF, "bitrateMode": cfg.BitrateMode, + "bitratePreset": cfg.BitratePreset, "videoBitrate": cfg.VideoBitrate, "targetFileSize": cfg.TargetFileSize, "targetResolution": cfg.TargetResolution, @@ -955,6 +957,7 @@ func (s *appState) batchAddToQueue(paths []string) { "encoderPreset": s.convert.EncoderPreset, "crf": s.convert.CRF, "bitrateMode": s.convert.BitrateMode, + "bitratePreset": s.convert.BitratePreset, "videoBitrate": s.convert.VideoBitrate, "targetResolution": s.convert.TargetResolution, "frameRate": s.convert.FrameRate, @@ -1534,6 +1537,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre // Execute FFmpeg cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...) + utils.ApplyNoWindow(cmd) stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("failed to create stdout pipe: %w", err) @@ -1798,6 +1802,7 @@ func runGUI() { EncoderPreset: "medium", CRF: "", // Empty means use Quality preset BitrateMode: "CRF", + BitratePreset: "Manual", VideoBitrate: "5000k", TargetResolution: "Source", FrameRate: "Source", @@ -2085,11 +2090,57 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { // Placeholder for updateDVDOptions - will be defined after resolution/framerate selects are created var updateDVDOptions func() - qualitySelect := widget.NewSelect([]string{"Draft (CRF 28)", "Standard (CRF 23)", "High (CRF 18)", "Lossless"}, func(value string) { - logging.Debug(logging.CatUI, "quality preset %s", value) + // Forward declarations for encoding controls (used in reset/update callbacks) + var ( + bitrateModeSelect *widget.Select + bitratePresetSelect *widget.Select + crfEntry *widget.Entry + videoBitrateEntry *widget.Entry + targetFileSizeSelect *widget.Select + targetFileSizeEntry *widget.Entry + qualitySelectSimple *widget.Select + qualitySelectAdv *widget.Select + qualitySectionSimple fyne.CanvasObject + qualitySectionAdv fyne.CanvasObject + ) + + qualityOptions := []string{"Draft (CRF 28)", "Standard (CRF 23)", "High (CRF 18)", "Lossless"} + var syncingQuality bool + + qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) { + if syncingQuality { + return + } + syncingQuality = true + logging.Debug(logging.CatUI, "quality preset %s (simple)", value) state.convert.Quality = value + if qualitySelectAdv != nil { + qualitySelectAdv.SetSelected(value) + } + if updateEncodingControls != nil { + updateEncodingControls() + } + syncingQuality = false }) - qualitySelect.SetSelected(state.convert.Quality) + + qualitySelectAdv = widget.NewSelect(qualityOptions, func(value string) { + if syncingQuality { + return + } + syncingQuality = true + logging.Debug(logging.CatUI, "quality preset %s (advanced)", value) + state.convert.Quality = value + if qualitySelectSimple != nil { + qualitySelectSimple.SetSelected(value) + } + if updateEncodingControls != nil { + updateEncodingControls() + } + syncingQuality = false + }) + + qualitySelectSimple.SetSelected(state.convert.Quality) + qualitySelectAdv.SetSelected(state.convert.Quality) outputEntry := widget.NewEntry() outputEntry.SetText(state.convert.OutputBase) @@ -2123,6 +2174,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { autoNameTemplate := widget.NewEntry() autoNameTemplate.SetPlaceHolder(" - - ") autoNameTemplate.SetText(state.convert.AutoNameTemplate) + autoNameTemplate.OnChanged = func(val string) { state.convert.AutoNameTemplate = val if state.convert.UseAutoNaming { @@ -2306,6 +2358,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { videoCodecSelect := widget.NewSelect([]string{"H.264", "H.265", "VP9", "AV1", "Copy"}, func(value string) { state.convert.VideoCodec = value logging.Debug(logging.CatUI, "video codec set to %s", value) + if updateQualityVisibility != nil { + updateQualityVisibility() + } }) videoCodecSelect.SetSelected(state.convert.VideoCodec) @@ -2328,6 +2383,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } } + var updateQualityVisibility func() + formatSelect := widget.NewSelect(formatLabels, func(value string) { for _, opt := range formatOptions { if opt.Label == value { @@ -2344,6 +2401,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { state.convert.VideoCodec = newCodec videoCodecSelect.SetSelected(newCodec) } + if updateQualityVisibility != nil { + updateQualityVisibility() + } break } } @@ -2412,6 +2472,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { VideoCodec: "H.264", EncoderPreset: "medium", BitrateMode: "CRF", + BitratePreset: "Manual", CRF: "", VideoBitrate: "", TargetResolution: "Source", @@ -2427,11 +2488,24 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { logging.Debug(logging.CatUI, "settings reset to defaults") formatSelect.SetSelected(state.convert.SelectedFormat.Label) videoCodecSelect.SetSelected(state.convert.VideoCodec) - qualitySelect.SetSelected(state.convert.Quality) + qualitySelectSimple.SetSelected(state.convert.Quality) + qualitySelectAdv.SetSelected(state.convert.Quality) simplePresetSelect.SetSelected(state.convert.EncoderPreset) + bitrateModeSelect.SetSelected(state.convert.BitrateMode) + bitratePresetSelect.SetSelected(state.convert.BitratePreset) + crfEntry.SetText(state.convert.CRF) + videoBitrateEntry.SetText(state.convert.VideoBitrate) + targetFileSizeSelect.SetSelected("Manual") + targetFileSizeEntry.SetText(state.convert.TargetFileSize) autoNameCheck.SetChecked(state.convert.UseAutoNaming) autoNameTemplate.SetText(state.convert.AutoNameTemplate) outputEntry.SetText(state.convert.OutputBase) + if updateEncodingControls != nil { + updateEncodingControls() + } + if updateQualityVisibility != nil { + updateQualityVisibility() + } }) resetSettingsBtn.Importance = widget.LowImportance @@ -2461,15 +2535,21 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { widget.NewSeparator(), ) + // Shared updater for bitrate/quality UI state; defined later alongside controls + var updateEncodingControls func() + // Bitrate Mode - bitrateModeSelect := widget.NewSelect([]string{"CRF", "CBR", "VBR", "Target Size"}, func(value string) { + bitrateModeSelect = widget.NewSelect([]string{"CRF", "CBR", "VBR", "Target Size"}, func(value string) { state.convert.BitrateMode = value logging.Debug(logging.CatUI, "bitrate mode set to %s", value) + if updateEncodingControls != nil { + updateEncodingControls() + } }) bitrateModeSelect.SetSelected(state.convert.BitrateMode) // Manual CRF entry - crfEntry := widget.NewEntry() + crfEntry = widget.NewEntry() crfEntry.SetPlaceHolder("Auto (from Quality preset)") crfEntry.SetText(state.convert.CRF) crfEntry.OnChanged = func(val string) { @@ -2477,18 +2557,52 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } // Video Bitrate entry (for CBR/VBR) - videoBitrateEntry := widget.NewEntry() + videoBitrateEntry = widget.NewEntry() videoBitrateEntry.SetPlaceHolder("5000k") videoBitrateEntry.SetText(state.convert.VideoBitrate) videoBitrateEntry.OnChanged = func(val string) { state.convert.VideoBitrate = val } - // Target File Size with smart presets + manual entry - targetFileSizeEntry := widget.NewEntry() - targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB") + type bitratePreset struct { + Label string + Bitrate string + Codec string + } - var targetFileSizeSelect *widget.Select + presets := []bitratePreset{ + {Label: "Manual", Bitrate: "", Codec: ""}, + {Label: "AV1 1080p - 1200k (smallest)", Bitrate: "1200k", Codec: "AV1"}, + {Label: "AV1 1080p - 1400k (sweet spot)", Bitrate: "1400k", Codec: "AV1"}, + {Label: "AV1 1080p - 1800k (headroom)", Bitrate: "1800k", Codec: "AV1"}, + {Label: "H.265 1080p - 2000k (balanced)", Bitrate: "2000k", Codec: "H.265"}, + {Label: "H.265 1080p - 2400k (noisy sources)", Bitrate: "2400k", Codec: "H.265"}, + {Label: "AV1 4K - 7M (archive)", Bitrate: "7000k", Codec: "AV1"}, + {Label: "H.265 4K - 9M (fast/Topaz)", Bitrate: "9000k", Codec: "H.265"}, + } + + bitratePresetLookup := make(map[string]bitratePreset) + var bitratePresetLabels []string + for _, p := range presets { + bitratePresetLookup[p.Label] = p + bitratePresetLabels = append(bitratePresetLabels, p.Label) + } + + var applyBitratePreset func(string) + + bitratePresetSelect = widget.NewSelect(bitratePresetLabels, func(value string) { + if applyBitratePreset != nil { + applyBitratePreset(value) + } + }) + if state.convert.BitratePreset == "" || bitratePresetLookup[state.convert.BitratePreset].Label == "" { + state.convert.BitratePreset = "Manual" + } + bitratePresetSelect.SetSelected(state.convert.BitratePreset) + + // Target File Size with smart presets + manual entry + targetFileSizeEntry = widget.NewEntry() + targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB") updateTargetSizeOptions := func() { if src == nil { @@ -2558,6 +2672,97 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { state.convert.TargetFileSize = val } + encodingHint := widget.NewLabel("") + encodingHint.Wrapping = fyne.TextWrapWord + + applyBitratePreset = func(label string) { + preset, ok := bitratePresetLookup[label] + if !ok { + label = "Manual" + preset = bitratePresetLookup[label] + } + + state.convert.BitratePreset = label + + // Move to CBR for predictable output when a preset is chosen + if preset.Bitrate != "" && state.convert.BitrateMode != "CBR" && state.convert.BitrateMode != "VBR" { + state.convert.BitrateMode = "CBR" + bitrateModeSelect.SetSelected("CBR") + } + + if preset.Bitrate != "" { + state.convert.VideoBitrate = preset.Bitrate + videoBitrateEntry.SetText(preset.Bitrate) + } + + // Adjust codec to match the preset intent (user can change back) + if preset.Codec != "" && state.convert.VideoCodec != preset.Codec { + state.convert.VideoCodec = preset.Codec + videoCodecSelect.SetSelected(preset.Codec) + } + + if updateEncodingControls != nil { + updateEncodingControls() + } + } + + updateEncodingControls = func() { + mode := state.convert.BitrateMode + isLossless := state.convert.Quality == "Lossless" + + // Default: enable everything + crfEntry.Enable() + videoBitrateEntry.Enable() + targetFileSizeEntry.Enable() + targetFileSizeSelect.Enable() + bitratePresetSelect.Enable() + + hint := "" + + if isLossless { + // Lossless forces CRF 0; ignore bitrate/preset/target size to reduce confusion + if mode != "CRF" { + state.convert.BitrateMode = "CRF" + bitrateModeSelect.SetSelected("CRF") + mode = "CRF" + } + if crfEntry.Text != "0" { + crfEntry.SetText("0") + } + state.convert.CRF = "0" + crfEntry.Disable() + videoBitrateEntry.Disable() + targetFileSizeEntry.Disable() + targetFileSizeSelect.Disable() + bitratePresetSelect.Disable() + hint = "Lossless forces CRF 0 for H.265/AV1; bitrate and target size are ignored." + } else { + switch mode { + case "CRF", "": + videoBitrateEntry.Disable() + targetFileSizeEntry.Disable() + targetFileSizeSelect.Disable() + bitratePresetSelect.Disable() + hint = "CRF mode uses the quality preset/CRF only." + case "CBR", "VBR": + crfEntry.Disable() + targetFileSizeEntry.Disable() + targetFileSizeSelect.Disable() + hint = "Bitrate mode uses the value above; presets auto-fill common choices." + case "Target Size": + crfEntry.Disable() + videoBitrateEntry.Disable() + bitratePresetSelect.Disable() + targetFileSizeEntry.Enable() + targetFileSizeSelect.Enable() + hint = "Target size calculates bitrate automatically from duration." + } + } + + encodingHint.SetText(hint) + } + updateEncodingControls() + // Target Resolution resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) { state.convert.TargetResolution = value @@ -2703,6 +2908,35 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } updateDVDOptions() + qualitySectionSimple = container.NewVBox( + widget.NewLabelWithStyle("═══ QUALITY ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + qualitySelectSimple, + ) + qualitySectionAdv = container.NewVBox( + widget.NewLabelWithStyle("Quality Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + qualitySelectAdv, + ) + + updateQualityVisibility = func() { + hide := strings.Contains(strings.ToLower(state.convert.SelectedFormat.Label), "h.265") || + strings.EqualFold(state.convert.VideoCodec, "H.265") + + if qualitySectionSimple != nil { + if hide { + qualitySectionSimple.Hide() + } else { + qualitySectionSimple.Show() + } + } + if qualitySectionAdv != nil { + if hide { + qualitySectionAdv.Hide() + } else { + qualitySectionAdv.Show() + } + } + } + // Simple mode options - minimal controls, aspect locked to Source simpleOptions := container.NewVBox( widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), @@ -2711,13 +2945,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { dvdAspectBox, // DVD options appear here when DVD format selected widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), outputEntry, - autoNameCheck, - autoNameTemplate, - autoNameHint, outputHint, widget.NewSeparator(), - widget.NewLabelWithStyle("═══ QUALITY ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), - qualitySelect, + qualitySectionSimple, widget.NewLabelWithStyle("Encoder Speed/Quality", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewLabel("Choose slower for better compression, faster for speed"), widget.NewLabelWithStyle("Encoder Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), @@ -2734,9 +2964,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { dvdAspectBox, // DVD options appear here when DVD format selected widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), outputEntry, - autoNameCheck, - autoNameTemplate, - autoNameHint, outputHint, coverDisplay, widget.NewSeparator(), @@ -2747,14 +2974,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { widget.NewLabelWithStyle("Encoder Preset (speed vs quality)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), encoderPresetSelect, encoderPresetHint, - widget.NewLabelWithStyle("Quality Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - qualitySelect, + qualitySectionAdv, widget.NewLabelWithStyle("Bitrate Mode", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), bitrateModeSelect, widget.NewLabelWithStyle("Manual CRF (overrides Quality preset)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), crfEntry, widget.NewLabelWithStyle("Video Bitrate (for CBR/VBR)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), videoBitrateEntry, + widget.NewLabelWithStyle("Recommended Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + bitratePresetSelect, + encodingHint, widget.NewLabelWithStyle("Target File Size", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), targetFileSizeSelect, targetFileSizeEntry, @@ -2815,6 +3044,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { advancedScrollBox := container.NewVScroll(advancedOptions) advancedScrollBox.SetMinSize(fyne.NewSize(0, 0)) + if updateQualityVisibility != nil { + updateQualityVisibility() + } + tabs := container.NewAppTabs( container.NewTabItem("Simple", simpleScrollBox), container.NewTabItem("Advanced", advancedScrollBox), @@ -2881,10 +3114,18 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { tabs.SelectIndex(0) // Select Simple tab state.convert.Mode = "Simple" formatSelect.SetSelected("MP4 (H.264)") - qualitySelect.SetSelected("Standard (CRF 23)") + state.convert.Quality = "Standard (CRF 23)" + qualitySelectSimple.SetSelected("Standard (CRF 23)") + qualitySelectAdv.SetSelected("Standard (CRF 23)") aspectOptions.SetSelected("Auto") targetAspectSelect.SetSelected("Source") updateAspectBoxVisibility() + if updateEncodingControls != nil { + updateEncodingControls() + } + if updateQualityVisibility != nil { + updateQualityVisibility() + } logging.Debug(logging.CatUI, "convert settings reset to defaults") }) statusLabel := widget.NewLabel("") @@ -3790,6 +4031,7 @@ func (p *playSession) runVideo(offset float64) { "-", } cmd := exec.Command(platformConfig.FFmpegPath, args...) + utils.ApplyNoWindow(cmd) cmd.Stderr = &stderr stdout, err := cmd.StdoutPipe() if err != nil { @@ -3877,6 +4119,7 @@ func (p *playSession) runAudio(offset float64) { "-f", "s16le", "-", ) + utils.ApplyNoWindow(cmd) cmd.Stderr = &stderr stdout, err := cmd.StdoutPipe() if err != nil { @@ -4610,6 +4853,7 @@ func detectBestH264Encoder() string { for _, encoder := range encoders { cmd := exec.Command(platformConfig.FFmpegPath, "-hide_banner", "-encoders") + utils.ApplyNoWindow(cmd) output, err := cmd.CombinedOutput() if err == nil { // Check if encoder is in the output @@ -4622,6 +4866,7 @@ func detectBestH264Encoder() string { // Fallback: check if libx264 is available cmd := exec.Command(platformConfig.FFmpegPath, "-hide_banner", "-encoders") + utils.ApplyNoWindow(cmd) output, err := cmd.CombinedOutput() if err == nil && (strings.Contains(string(output), " libx264 ") || strings.Contains(string(output), " libx264\n")) { logging.Debug(logging.CatFFMPEG, "using software encoder: libx264") @@ -4638,6 +4883,7 @@ func detectBestH265Encoder() string { for _, encoder := range encoders { cmd := exec.Command(platformConfig.FFmpegPath, "-hide_banner", "-encoders") + utils.ApplyNoWindow(cmd) output, err := cmd.CombinedOutput() if err == nil { if strings.Contains(string(output), " "+encoder+" ") || strings.Contains(string(output), " "+encoder+"\n") { @@ -4648,6 +4894,7 @@ func detectBestH265Encoder() string { } cmd := exec.Command(platformConfig.FFmpegPath, "-hide_banner", "-encoders") + utils.ApplyNoWindow(cmd) output, err := cmd.CombinedOutput() if err == nil && (strings.Contains(string(output), " libx265 ") || strings.Contains(string(output), " libx265\n")) { logging.Debug(logging.CatFFMPEG, "using software encoder: libx265") @@ -5147,6 +5394,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But started := time.Now() cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...) + utils.ApplyNoWindow(cmd) stdout, err := cmd.StdoutPipe() if err != nil { logging.Debug(logging.CatFFMPEG, "convert stdout pipe failed: %v", err) @@ -5625,6 +5873,7 @@ func (s *appState) generateSnippet() { args = append(args, outPath) cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...) + utils.ApplyNoWindow(cmd) logging.Debug(logging.CatFFMPEG, "snippet command: %s", strings.Join(cmd.Args, " ")) // Show progress dialog for snippets that need re-encoding (WMV, filters, etc.) @@ -5672,6 +5921,7 @@ func capturePreviewFrames(path string, duration float64) ([]string, error) { "-vf", "scale=640:-1:flags=lanczos,fps=8", pattern, ) + utils.ApplyNoWindow(cmd) out, err := cmd.CombinedOutput() if err != nil { os.RemoveAll(dir) @@ -5776,6 +6026,7 @@ func probeVideo(path string) (*videoSource, error) { "-show_streams", path, ) + utils.ApplyNoWindow(cmd) out, err := cmd.Output() if err != nil { return nil, err @@ -5894,6 +6145,7 @@ func probeVideo(path string) (*videoSource, error) { "-y", coverPath, ) + utils.ApplyNoWindow(extractCmd) if err := extractCmd.Run(); err != nil { logging.Debug(logging.CatFFMPEG, "failed to extract embedded cover art: %v", err) } else { @@ -5958,6 +6210,7 @@ func detectCrop(path string, duration float64) *CropValues { "-f", "null", "-", ) + utils.ApplyNoWindow(cmd) output, err := cmd.CombinedOutput() if err != nil { @@ -6561,7 +6814,7 @@ func buildInspectView(state *appState) fyne.CanvasObject { }) clearBtn.Importance = widget.LowImportance - instructionsRow := container.NewBorder(nil, nil, nil, clearBtn, instructions) + instructionsRow := container.NewBorder(nil, nil, nil, nil, instructions) // File label fileLabel := widget.NewLabel("No file loaded") diff --git a/platform.go b/platform.go index e3c09c6..af121c9 100644 --- a/platform.go +++ b/platform.go @@ -10,19 +10,20 @@ import ( "time" "git.leaktechnologies.dev/stu/VideoTools/internal/logging" + "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) // PlatformConfig holds platform-specific configuration type PlatformConfig struct { - FFmpegPath string - FFprobePath string - TempDir string - HWEncoders []string - ExeExtension string - PathSeparator string - IsWindows bool - IsLinux bool - IsDarwin bool + FFmpegPath string + FFprobePath string + TempDir string + HWEncoders []string + ExeExtension string + PathSeparator string + IsWindows bool + IsLinux bool + IsDarwin bool } // DetectPlatform detects the current platform and returns configuration @@ -167,6 +168,7 @@ func detectHardwareEncoders(cfg *PlatformConfig) []string { // Get list of available encoders from ffmpeg cmd := exec.Command(cfg.FFmpegPath, "-hide_banner", "-encoders") + utils.ApplyNoWindow(cmd) output, err := cmd.Output() if err != nil { logging.Debug(logging.CatSystem, "Failed to query ffmpeg encoders: %v", err)