This commit includes three critical bug fixes and Windows build improvements: **Bug Fixes:** 1. **Queue Conversion Progress Tracking** (main.go:1471-1534) - Enhanced executeConvertJob() to parse FPS, speed, and ETA from FFmpeg output - Queue jobs now show detailed progress metrics matching direct conversions - Stats stored in job.Config for display in the conversion stats bar 2. **AMD AMF Hardware Acceleration** (main.go) - Added "amf" to hardware acceleration options - Support for h264_amf, hevc_amf, and av1_amf encoders - Added AMF-specific error detection in FFmpeg output parsing 3. **DVD Format Resolution Forcing** (main.go:1080-1103, 4504-4517) - Removed automatic resolution forcing when DVD format is selected - Removed -target parameter usage which was forcing 720×480/720×576 - Resolution now defaults to "Source" unless explicitly changed - DVD compliance maintained through manual bitrate/GOP/codec parameters **Windows Build Improvements:** - Updated build.bat to enable CGO (required for Fyne/OpenGL) - Added automatic GCC/MinGW-w64 detection and installation - Automated setup via winget for one-command Windows builds - Improved error messages with fallback manual instructions **Documentation:** - Added comprehensive Windows setup guides - Created platform.go for future platform-specific code - Updated .gitignore for Windows build artifacts All changes tested and working. Ready for production use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
329 lines
8.5 KiB
Go
329 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// DetectPlatform detects the current platform and returns configuration
|
|
func DetectPlatform() *PlatformConfig {
|
|
cfg := &PlatformConfig{
|
|
IsWindows: runtime.GOOS == "windows",
|
|
IsLinux: runtime.GOOS == "linux",
|
|
IsDarwin: runtime.GOOS == "darwin",
|
|
PathSeparator: string(filepath.Separator),
|
|
}
|
|
|
|
if cfg.IsWindows {
|
|
cfg.ExeExtension = ".exe"
|
|
}
|
|
|
|
cfg.FFmpegPath = findFFmpeg(cfg)
|
|
cfg.FFprobePath = findFFprobe(cfg)
|
|
cfg.TempDir = getTempDir(cfg)
|
|
cfg.HWEncoders = detectHardwareEncoders(cfg)
|
|
|
|
logging.Debug(logging.CatSystem, "Platform detected: %s/%s", runtime.GOOS, runtime.GOARCH)
|
|
logging.Debug(logging.CatSystem, "FFmpeg path: %s", cfg.FFmpegPath)
|
|
logging.Debug(logging.CatSystem, "FFprobe path: %s", cfg.FFprobePath)
|
|
logging.Debug(logging.CatSystem, "Temp directory: %s", cfg.TempDir)
|
|
logging.Debug(logging.CatSystem, "Hardware encoders: %v", cfg.HWEncoders)
|
|
|
|
return cfg
|
|
}
|
|
|
|
// findFFmpeg locates the ffmpeg executable
|
|
func findFFmpeg(cfg *PlatformConfig) string {
|
|
exeName := "ffmpeg"
|
|
if cfg.IsWindows {
|
|
exeName = "ffmpeg.exe"
|
|
}
|
|
|
|
// Priority 1: Bundled with application
|
|
if exePath, err := os.Executable(); err == nil {
|
|
bundled := filepath.Join(filepath.Dir(exePath), exeName)
|
|
if _, err := os.Stat(bundled); err == nil {
|
|
logging.Debug(logging.CatSystem, "Found bundled ffmpeg: %s", bundled)
|
|
return bundled
|
|
}
|
|
}
|
|
|
|
// Priority 2: Environment variable
|
|
if envPath := os.Getenv("FFMPEG_PATH"); envPath != "" {
|
|
if _, err := os.Stat(envPath); err == nil {
|
|
logging.Debug(logging.CatSystem, "Found ffmpeg from FFMPEG_PATH: %s", envPath)
|
|
return envPath
|
|
}
|
|
}
|
|
|
|
// Priority 3: System PATH
|
|
if path, err := exec.LookPath(exeName); err == nil {
|
|
logging.Debug(logging.CatSystem, "Found ffmpeg in PATH: %s", path)
|
|
return path
|
|
}
|
|
|
|
// Priority 4: Common install locations (Windows)
|
|
if cfg.IsWindows {
|
|
commonPaths := []string{
|
|
filepath.Join(os.Getenv("ProgramFiles"), "ffmpeg", "bin", "ffmpeg.exe"),
|
|
filepath.Join(os.Getenv("ProgramFiles(x86)"), "ffmpeg", "bin", "ffmpeg.exe"),
|
|
`C:\ffmpeg\bin\ffmpeg.exe`,
|
|
}
|
|
for _, path := range commonPaths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
logging.Debug(logging.CatSystem, "Found ffmpeg at common location: %s", path)
|
|
return path
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: assume it's in PATH (will error later if not found)
|
|
logging.Debug(logging.CatSystem, "FFmpeg not found, using fallback: %s", exeName)
|
|
return exeName
|
|
}
|
|
|
|
// findFFprobe locates the ffprobe executable
|
|
func findFFprobe(cfg *PlatformConfig) string {
|
|
exeName := "ffprobe"
|
|
if cfg.IsWindows {
|
|
exeName = "ffprobe.exe"
|
|
}
|
|
|
|
// Priority 1: Same directory as ffmpeg
|
|
ffmpegDir := filepath.Dir(cfg.FFmpegPath)
|
|
if ffmpegDir != "." && ffmpegDir != "" {
|
|
probePath := filepath.Join(ffmpegDir, exeName)
|
|
if _, err := os.Stat(probePath); err == nil {
|
|
return probePath
|
|
}
|
|
}
|
|
|
|
// Priority 2: Bundled with application
|
|
if exePath, err := os.Executable(); err == nil {
|
|
bundled := filepath.Join(filepath.Dir(exePath), exeName)
|
|
if _, err := os.Stat(bundled); err == nil {
|
|
return bundled
|
|
}
|
|
}
|
|
|
|
// Priority 3: System PATH
|
|
if path, err := exec.LookPath(exeName); err == nil {
|
|
return path
|
|
}
|
|
|
|
// Fallback
|
|
return exeName
|
|
}
|
|
|
|
// getTempDir returns platform-appropriate temp directory
|
|
func getTempDir(cfg *PlatformConfig) string {
|
|
var base string
|
|
|
|
if cfg.IsWindows {
|
|
// Windows: Use AppData\Local\Temp\VideoTools
|
|
appData := os.Getenv("LOCALAPPDATA")
|
|
if appData != "" {
|
|
base = filepath.Join(appData, "Temp", "VideoTools")
|
|
} else {
|
|
base = filepath.Join(os.TempDir(), "VideoTools")
|
|
}
|
|
} else {
|
|
// Linux/macOS: Use /tmp/videotools
|
|
base = filepath.Join(os.TempDir(), "videotools")
|
|
}
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(base, 0755); err != nil {
|
|
logging.Debug(logging.CatSystem, "Failed to create temp directory %s: %v", base, err)
|
|
return os.TempDir()
|
|
}
|
|
|
|
return base
|
|
}
|
|
|
|
// detectHardwareEncoders detects available hardware encoders
|
|
func detectHardwareEncoders(cfg *PlatformConfig) []string {
|
|
var encoders []string
|
|
|
|
// Get list of available encoders from ffmpeg
|
|
cmd := exec.Command(cfg.FFmpegPath, "-hide_banner", "-encoders")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "Failed to query ffmpeg encoders: %v", err)
|
|
return encoders
|
|
}
|
|
|
|
encoderList := string(output)
|
|
|
|
// Platform-specific encoder detection
|
|
if cfg.IsWindows {
|
|
// Windows: Check for NVENC, QSV, AMF
|
|
if strings.Contains(encoderList, "h264_nvenc") {
|
|
encoders = append(encoders, "nvenc")
|
|
logging.Debug(logging.CatSystem, "Detected NVENC (NVIDIA) encoder")
|
|
}
|
|
if strings.Contains(encoderList, "h264_qsv") {
|
|
encoders = append(encoders, "qsv")
|
|
logging.Debug(logging.CatSystem, "Detected QSV (Intel) encoder")
|
|
}
|
|
if strings.Contains(encoderList, "h264_amf") {
|
|
encoders = append(encoders, "amf")
|
|
logging.Debug(logging.CatSystem, "Detected AMF (AMD) encoder")
|
|
}
|
|
} else if cfg.IsLinux {
|
|
// Linux: Check for VAAPI, NVENC, QSV
|
|
if strings.Contains(encoderList, "h264_vaapi") {
|
|
encoders = append(encoders, "vaapi")
|
|
logging.Debug(logging.CatSystem, "Detected VAAPI encoder")
|
|
}
|
|
if strings.Contains(encoderList, "h264_nvenc") {
|
|
encoders = append(encoders, "nvenc")
|
|
logging.Debug(logging.CatSystem, "Detected NVENC encoder")
|
|
}
|
|
if strings.Contains(encoderList, "h264_qsv") {
|
|
encoders = append(encoders, "qsv")
|
|
logging.Debug(logging.CatSystem, "Detected QSV encoder")
|
|
}
|
|
} else if cfg.IsDarwin {
|
|
// macOS: Check for VideoToolbox, NVENC
|
|
if strings.Contains(encoderList, "h264_videotoolbox") {
|
|
encoders = append(encoders, "videotoolbox")
|
|
logging.Debug(logging.CatSystem, "Detected VideoToolbox encoder")
|
|
}
|
|
if strings.Contains(encoderList, "h264_nvenc") {
|
|
encoders = append(encoders, "nvenc")
|
|
logging.Debug(logging.CatSystem, "Detected NVENC encoder")
|
|
}
|
|
}
|
|
|
|
return encoders
|
|
}
|
|
|
|
// ValidateWindowsPath validates Windows-specific path constraints
|
|
func ValidateWindowsPath(path string) error {
|
|
if runtime.GOOS != "windows" {
|
|
return nil
|
|
}
|
|
|
|
if len(path) == 0 {
|
|
return fmt.Errorf("empty path")
|
|
}
|
|
|
|
// Check for drive letter (C:, D:, etc.)
|
|
if len(path) >= 2 && path[1] == ':' {
|
|
drive := strings.ToUpper(string(path[0]))
|
|
if drive < "A" || drive > "Z" {
|
|
return fmt.Errorf("invalid drive letter: %s", drive)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check for UNC path (\\server\share)
|
|
if strings.HasPrefix(path, `\\`) || strings.HasPrefix(path, `//`) {
|
|
parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(path, `\\`), `//`), `\`)
|
|
if len(parts) < 2 {
|
|
return fmt.Errorf("invalid UNC path: %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Relative path is OK
|
|
return nil
|
|
}
|
|
|
|
// KillProcess kills a process in a platform-appropriate way
|
|
func KillProcess(cmd *exec.Cmd) error {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return nil
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// Windows: Kill directly (no SIGTERM support)
|
|
return cmd.Process.Kill()
|
|
}
|
|
|
|
// Unix: Try graceful shutdown first
|
|
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
|
return cmd.Process.Kill()
|
|
}
|
|
|
|
// Give it a moment to shut down gracefully
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-time.After(2 * time.Second):
|
|
// Timeout, force kill
|
|
return cmd.Process.Kill()
|
|
}
|
|
}
|
|
|
|
// GetEncoderName returns the full encoder name for a given hardware acceleration type and codec
|
|
func GetEncoderName(hwAccel, codec string) string {
|
|
if hwAccel == "none" || hwAccel == "" {
|
|
// Software encoding
|
|
switch codec {
|
|
case "H.264":
|
|
return "libx264"
|
|
case "H.265", "HEVC":
|
|
return "libx265"
|
|
case "VP9":
|
|
return "libvpx-vp9"
|
|
case "AV1":
|
|
return "libaom-av1"
|
|
default:
|
|
return "libx264"
|
|
}
|
|
}
|
|
|
|
// Hardware encoding
|
|
codecSuffix := ""
|
|
switch codec {
|
|
case "H.264":
|
|
codecSuffix = "h264"
|
|
case "H.265", "HEVC":
|
|
codecSuffix = "hevc"
|
|
default:
|
|
codecSuffix = "h264"
|
|
}
|
|
|
|
switch hwAccel {
|
|
case "nvenc":
|
|
return fmt.Sprintf("%s_nvenc", codecSuffix)
|
|
case "qsv":
|
|
return fmt.Sprintf("%s_qsv", codecSuffix)
|
|
case "vaapi":
|
|
return fmt.Sprintf("%s_vaapi", codecSuffix)
|
|
case "videotoolbox":
|
|
return fmt.Sprintf("%s_videotoolbox", codecSuffix)
|
|
case "amf":
|
|
return fmt.Sprintf("%s_amf", codecSuffix)
|
|
default:
|
|
return fmt.Sprintf("lib%s", strings.ToLower(codec))
|
|
}
|
|
}
|