VideoTools/platform.go

331 lines
8.6 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"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
}
// 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")
utils.ApplyNoWindow(cmd)
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))
}
}