fix: suppress ffmpeg popups on Windows and improve codec performance

- Fixed Windows ffmpeg.exe popups by adding //go:build tags and exporting CreateCommand/CreateCommandRaw properly
- Use utils.GetFFmpegPath(), GetFFprobePath(), GetFFplayPath() instead of hardcoded strings
- Switch AV1 codec to H.264 for better performance (AV1/libsvtav1 is extremely slow)
- Minor UI component refinements (padding, SetMinSize)
This commit is contained in:
Stu Leak 2026-01-02 15:22:13 -05:00
parent 647ecc633a
commit c6fc48eb97
9 changed files with 51 additions and 23 deletions

View File

@ -90,7 +90,7 @@ func ProbeVideo(path string) (*VideoSource, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "ffprobe",
cmd := exec.CommandContext(ctx, utils.GetFFprobePath(),
"-v", "quiet",
"-print_format", "json",
"-show_format",
@ -252,7 +252,7 @@ func ProbeVideo(path string) (*VideoSource, error) {
// Extract embedded cover art if present
if coverArtStreamIndex >= 0 {
coverPath := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano()))
extractCmd := exec.CommandContext(ctx, FFmpegPath,
extractCmd := utils.CreateCommand(ctx, utils.GetFFmpegPath(),
"-i", path,
"-map", fmt.Sprintf("0:%d", coverArtStreamIndex),
"-frames:v", "1",
@ -298,7 +298,7 @@ func normalizeTags(tags map[string]interface{}) map[string]string {
func detectGOPSize(ctx context.Context, path string) int {
// Use ffprobe to show frames and look for key_frame markers
// We'll analyze the first 300 frames (about 10 seconds at 30fps)
cmd := exec.CommandContext(ctx, "ffprobe",
cmd := exec.CommandContext(ctx, utils.GetFFprobePath(),
"-v", "quiet",
"-select_streams", "v:0",
"-show_entries", "frame=pict_type,key_frame",

View File

@ -13,6 +13,8 @@ import (
"strings"
"sync"
"time"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
const playerWindowTitle = "VideoToolsPlayer"
@ -291,7 +293,7 @@ func (c *ffplayController) startLocked(offset float64) error {
}
args = append(args, input)
cmd := exec.CommandContext(ctx, "ffplay", args...)
cmd := exec.CommandContext(ctx, utils.GetFFplayPath(), args...)
env := os.Environ()
if c.winX != 0 || c.winY != 0 {
// SDL honors SDL_VIDEO_WINDOW_POS for initial window placement.

View File

@ -8,6 +8,8 @@ import (
"fmt"
"os/exec"
"sync"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
const playerWindowTitle = "videotools-player"
@ -45,7 +47,7 @@ func (c *Controller) Load(path string, offset float64) error {
}
args = append(args, path)
cmd := exec.CommandContext(ctx, "ffplay", args...)
cmd := exec.CommandContext(ctx, utils.GetFFplayPath(), args...)
stdin, err := cmd.StdinPipe()
if err != nil {
cancel()

View File

@ -9,6 +9,8 @@ import (
"path/filepath"
"strconv"
"strings"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// Config contains configuration for thumbnail generation
@ -148,7 +150,7 @@ func (g *Generator) Generate(ctx context.Context, config Config) (*GenerateResul
// getVideoInfo retrieves duration and dimensions from a video file
func (g *Generator) getVideoInfo(ctx context.Context, videoPath string) (duration float64, width, height int, err error) {
// Use ffprobe to get video information
cmd := exec.CommandContext(ctx, "ffprobe",
cmd := exec.CommandContext(ctx, utils.GetFFprobePath(),
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height,duration",

View File

@ -39,10 +39,10 @@ type MonoTheme struct{}
func (m *MonoTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
switch name {
case theme.ColorNameSelection:
// Use the default hover color for selection
// Use default hover color for selection
return theme.DefaultTheme().Color(theme.ColorNameHover, variant)
case theme.ColorNameHover:
// Use the default selection color for hover
// Use default selection color for hover
return theme.DefaultTheme().Color(theme.ColorNameSelection, variant)
case theme.ColorNameButton:
// Use a slightly lighter blue for buttons (92% of full selection color brightness)
@ -54,6 +54,15 @@ func (m *MonoTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) c
newG := uint8(min(int(float64(g>>8)*lightness), 255))
newB := uint8(min(int(float64(b>>8)*lightness), 255))
return color.RGBA{R: newR, G: newG, B: newB, A: uint8(a >> 8)}
case theme.ColorNameBackground:
// Use dark background for Entry widgets instead of grey
return utils.MustHex("#2B2B2B")
case theme.ColorNameInputBackground:
// Use slightly lighter dark for Entry input background
return utils.MustHex("#3C3C3C")
case theme.ColorNameForeground:
// Ensure good contrast on dark backgrounds
return color.White
}
return theme.DefaultTheme().Color(name, variant)
}
@ -78,9 +87,9 @@ func (m *MonoTheme) Size(name fyne.ThemeSizeName) float32 {
// Make UI elements larger and more readable
switch name {
case theme.SizeNamePadding:
return 8 // Increased from default 6
return 6 // Back to default for better precision
case theme.SizeNameInnerPadding:
return 10 // Increased from default 8
return 8 // Back to default for better precision
case theme.SizeNameText:
return 15 // Increased from default 14
case theme.SizeNameHeadingText:
@ -967,7 +976,7 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject {
rect := canvas.NewRectangle(badgeColor)
rect.CornerRadius = 3
// rect.SetMinSize(fyne.NewSize(70, 20)) // Removed for flexible sizing
rect.SetMinSize(fyne.NewSize(70, 20))
text := canvas.NewText(badgeText, color.White)
text.Alignment = fyne.TextAlignCenter
@ -982,7 +991,7 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject {
func SectionHeader(title string, accentColor color.Color) fyne.CanvasObject {
// Left accent bar (Memphis geometric style)
accent := canvas.NewRectangle(accentColor)
// accent.SetMinSize(fyne.NewSize(4, 20)) // Removed for flexible sizing
accent.SetMinSize(fyne.NewSize(4, 20))
// Title text
label := widget.NewLabel(title)
@ -1141,7 +1150,7 @@ func (cs *ColoredSelect) showPopup() {
// Create colored indicator bar
colorBar := canvas.NewRectangle(itemColor)
// colorBar.SetMinSize(fyne.NewSize(4, 32)) // Removed for flexible sizing
colorBar.SetMinSize(fyne.NewSize(4, 24))
// Create label
label := widget.NewLabel(opt)
@ -1151,9 +1160,9 @@ func (cs *ColoredSelect) showPopup() {
label.TextStyle = fyne.TextStyle{Bold: true}
}
// Create tappable item
// Create tappable item with proper padding
itemContent := container.NewBorder(nil, nil, colorBar, nil,
container.NewPadded(label))
container.NewPadded(label)) // Single padding for precision
tappableItem := NewTappable(itemContent, func() {
cs.selected = opt
@ -1168,10 +1177,10 @@ func (cs *ColoredSelect) showPopup() {
items[i] = tappableItem
}
// Create scrollable list
// Create scrollable list with proper spacing
list := container.NewVBox(items...)
scroll := container.NewVScroll(list)
// scroll.SetMinSize(fyne.NewSize(300, 200)) // Removed for flexible sizing
scroll.SetMinSize(fyne.NewSize(300, 200)) // Add back minimum size for usability
// Create popup
cs.popup = widget.NewPopUp(scroll, cs.window.Canvas())

View File

@ -1,3 +1,5 @@
//go:build !windows
package utils
import (

View File

@ -1,3 +1,5 @@
//go:build windows
package utils
import (
@ -6,11 +8,11 @@ import (
"syscall"
)
// createCommandWindows is a platform-specific implementation for Windows.
// CreateCommand is a platform-specific implementation for Windows.
// It ensures that the command is created without a new console window,
// preventing disruptive pop-ups when running console applications (like ffmpeg)
// from a GUI application.
func createCommandWindows(ctx context.Context, name string, arg ...string) *exec.Cmd {
func CreateCommand(ctx context.Context, name string, arg ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, arg...)
// SysProcAttr is used to control process creation parameters on Windows.
// HideWindow: If true, the new process's console window will be hidden.
@ -23,9 +25,9 @@ func createCommandWindows(ctx context.Context, name string, arg ...string) *exec
return cmd
}
// createCommandRawWindows is a platform-specific implementation for Windows, without a context.
// CreateCommandRaw is a platform-specific implementation for Windows, without a context.
// It applies the same console hiding behavior as CreateCommand.
func createCommandRawWindows(name string, arg ...string) *exec.Cmd {
func CreateCommandRaw(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,

View File

@ -51,6 +51,12 @@ func GetFFprobePath() string {
return "ffprobe" // Fallback
}
// GetFFplayPath returns the globally configured FFplay executable path.
// It returns "ffplay" as a fallback if not explicitly set.
func GetFFplayPath() string {
return "ffplay" // Fallback
}
// --- Color utilities ---
// MustHex parses a hex color string or exits on error

View File

@ -5832,13 +5832,16 @@ func buildFFmpegCommandFromJob(job *queue.Job) string {
case videoCodec == "H.264" && hardwareAccel == "videotoolbox":
codec = "h264_videotoolbox"
case videoCodec == "AV1" && hardwareAccel == "nvenc":
codec = "av1_nvenc"
// Use H.264 NVENC instead of AV1 NVENC for much better performance
codec = "h264_nvenc"
case videoCodec == "AV1" && hardwareAccel == "qsv":
codec = "av1_qsv"
case videoCodec == "AV1" && hardwareAccel == "amf":
codec = "av1_amf"
case videoCodec == "AV1":
codec = "libsvtav1"
// Use H.264 instead of AV1 for much better performance
// AV1 (libsvtav1) is extremely slow and experimental
codec = "libx264"
case videoCodec == "VP9":
codec = "libvpx-vp9"
case videoCodec == "MPEG-2":