Files
ffmpeg-gui/app.go
T
2026-07-29 19:35:47 +08:00

365 lines
9.6 KiB
Go

package main
import (
"context"
"ffmpeg-gui/internal/config"
"ffmpeg-gui/internal/ffmpeg"
"ffmpeg-gui/internal/gpu"
"ffmpeg-gui/internal/hwaccel"
"ffmpeg-gui/internal/media"
"ffmpeg-gui/internal/platform"
"ffmpeg-gui/internal/task"
"fmt"
"os"
"strings"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App is the main application struct. Its exported methods are bound to the frontend.
type App struct {
ctx context.Context
cfg *config.Config
exec *ffmpeg.Executor
taskMgr *task.Manager
hwDetect *hwaccel.Detector
}
// NewApp creates a new App instance.
func NewApp() *App {
return &App{}
}
// startup is called when the app starts.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.cfg = config.Load()
runtime.LogInfo(ctx, "[app] config loaded")
// Detect ffmpeg/ffprobe (system → saved → bundled)
bins, err := detectBins(a.cfg)
if err != nil {
// System/PATH not found — extract embedded zip if bundled
zipData, zipErr := bundledDir.ReadFile("bundled/ffmpeg.zip")
runtime.LogInfo(ctx, fmt.Sprintf("ffmpeg not in PATH, bundled zip: err=%v size=%d", zipErr, len(zipData)))
if zipErr == nil {
ffmpegPath, ffprobePath := a.cfg.ExtractZip(zipData)
if ffmpegPath != "" {
a.cfg.FFmpegPath = ffmpegPath
}
if ffprobePath != "" {
a.cfg.FFprobePath = ffprobePath
}
if a.cfg.FFmpegPath != "" || a.cfg.FFprobePath != "" {
a.cfg.Save()
runtime.LogInfo(ctx, fmt.Sprintf("config saved with ffmpeg=%s ffprobe=%s", a.cfg.FFmpegPath, a.cfg.FFprobePath))
}
}
// Retry with extracted binaries
bins, err = detectBins(a.cfg)
}
if err != nil {
runtime.LogError(ctx, fmt.Sprintf("ffmpeg detect failed: %v", err))
return
}
runtime.LogInfo(ctx, fmt.Sprintf("ffmpeg: %s, ffprobe: %s", bins.FFmpeg, bins.FFprobe))
a.exec = ffmpeg.NewExecutor(bins)
a.hwDetect = hwaccel.NewDetector(a.exec)
a.taskMgr = task.NewManager(a.exec)
// Restore saved preferred accelerator
if a.cfg.PreferredAccel != "" {
a.taskMgr.SetHWAccel(a.cfg.PreferredAccel)
}
a.taskMgr.SetEventCallback(func(eventType string, data any) {
runtime.EventsEmit(ctx, eventType, data)
})
// Enable window resize borders on frameless windows (Windows only)
go func() {
time.Sleep(200 * time.Millisecond)
platform.EnableResizeBorder()
}()
}
// shutdown is called when the app is closing.
func (a *App) shutdown(ctx context.Context) {
if a.taskMgr != nil {
a.taskMgr.Stop()
}
}
// ---- Window Controls ----
// MinimizeWindow minimizes the application window.
func (a *App) MinimizeWindow() {
runtime.WindowMinimise(a.ctx)
}
// MaximizeWindow toggles the window between maximized and normal.
func (a *App) MaximizeWindow() {
if runtime.WindowIsMaximised(a.ctx) {
runtime.WindowUnmaximise(a.ctx)
} else {
runtime.WindowMaximise(a.ctx)
}
}
// CloseWindow closes the application.
func (a *App) CloseWindow() {
runtime.Quit(a.ctx)
}
// IsMaximised returns whether the window is currently maximized.
func (a *App) IsMaximised() bool {
return runtime.WindowIsMaximised(a.ctx)
}
// ---- Media Info ----
// GetMediaInfo retrieves media file information via ffprobe.
func (a *App) GetMediaInfo(inputFile string) (*media.MediaInfo, error) {
if a.exec == nil {
return nil, fmt.Errorf("ffmpeg not initialized")
}
return media.GetInfo(a.exec, inputFile)
}
// ---- Hardware Detection ----
// GetHardwareEncoders returns detected hardware encoders.
func (a *App) GetHardwareEncoders() ([]hwaccel.HWEncoder, error) {
if a.hwDetect == nil {
return nil, fmt.Errorf("hw detector not initialized")
}
return a.hwDetect.DetectEncoders()
}
// GetGPUInfo returns detected GPU models and their encoder capabilities.
func (a *App) GetGPUInfo() []gpu.GPUInfo {
return gpu.DetectGPUs(a.exec)
}
// detectBins finds ffmpeg/ffprobe, checking config paths first.
func detectBins(cfg *config.Config) (ffmpeg.BinPaths, error) {
// Try config-saved paths first
if cfg.FFmpegPath != "" && cfg.FFprobePath != "" {
if fileExists(cfg.FFmpegPath) && fileExists(cfg.FFprobePath) {
return ffmpeg.BinPaths{FFmpeg: cfg.FFmpegPath, FFprobe: cfg.FFprobePath}, nil
}
}
// Fall back to default detection (bundled, then PATH)
return ffmpeg.Detect()
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// ---- Config API ----
// GetConfig returns the current app configuration.
func (a *App) GetConfig() map[string]string {
return map[string]string{
"theme": a.cfg.Theme,
"preferredAccel": a.cfg.PreferredAccel,
"outputDir": a.cfg.OutputDir,
"namingRule": a.cfg.NamingRule,
}
}
// SaveConfig saves a configuration value.
func (a *App) SaveConfig(key, value string) {
switch key {
case "theme":
a.cfg.Theme = value
case "preferredAccel":
a.cfg.PreferredAccel = value
case "outputDir":
a.cfg.OutputDir = value
case "namingRule":
a.cfg.NamingRule = value
}
a.cfg.Save()
}
// ---- Hardware Detection ----
// CheckFFmpeg returns ffmpeg and ffprobe paths and version info.
func (a *App) CheckFFmpeg() map[string]string {
result := map[string]string{
"ffmpegPath": "",
"ffprobePath": "",
"ffmpegVer": "",
"ffprobeVer": "",
}
if a.exec == nil {
// Try config paths as fallback
bins, err := detectBins(a.cfg)
if err != nil {
return result
}
result["ffmpegPath"] = bins.FFmpeg
result["ffprobePath"] = bins.FFprobe
// Can't get version without executor, but at least show paths
return result
}
// Use saved config paths if available
if a.cfg.FFmpegPath != "" && fileExists(a.cfg.FFmpegPath) {
result["ffmpegPath"] = a.cfg.FFmpegPath
} else {
bins, _ := ffmpeg.Detect()
result["ffmpegPath"] = bins.FFmpeg
}
if a.cfg.FFprobePath != "" && fileExists(a.cfg.FFprobePath) {
result["ffprobePath"] = a.cfg.FFprobePath
} else {
bins, _ := ffmpeg.Detect()
result["ffprobePath"] = bins.FFprobe
}
out, err := a.exec.RunSync("-version")
if err == nil {
lines := strings.Split(out, "\n")
if len(lines) > 0 {
result["ffmpegVer"] = strings.TrimSpace(lines[0])
}
}
out, err = a.exec.Probe("-version")
if err == nil {
lines := strings.Split(out, "\n")
if len(lines) > 0 {
result["ffprobeVer"] = strings.TrimSpace(lines[0])
}
}
return result
}
// GetAccelerators returns detected hardware acceleration methods.
func (a *App) GetAccelerators() ([]hwaccel.Accelerator, error) {
if a.hwDetect == nil {
return nil, fmt.Errorf("hw detector not initialized")
}
return a.hwDetect.DetectAccelerators()
}
// ---- Task Management ----
// AddTask adds a new task to the queue and returns the task ID.
func (a *App) AddTask(t *task.Task) (string, error) {
if a.taskMgr == nil {
return "", fmt.Errorf("task manager not initialized")
}
id := a.taskMgr.Add(t)
return id, nil
}
// StartTask starts a specific task by ID.
func (a *App) StartTask(taskID string) error {
if a.taskMgr == nil {
return fmt.Errorf("task manager not initialized")
}
return a.taskMgr.Start(taskID)
}
// StartAllTasks starts all pending tasks sequentially.
func (a *App) StartAllTasks() {
if a.taskMgr != nil {
a.taskMgr.StartAll()
}
}
// CancelTask cancels a running or pending task.
func (a *App) CancelTask(taskID string) error {
if a.taskMgr == nil {
return fmt.Errorf("task manager not initialized")
}
return a.taskMgr.Cancel(taskID)
}
// RemoveTask removes a completed/failed/canceled task.
func (a *App) RemoveTask(taskID string) error {
if a.taskMgr == nil {
return fmt.Errorf("task manager not initialized")
}
return a.taskMgr.Remove(taskID)
}
// GetTasks returns all tasks in the queue.
func (a *App) GetTasks() []*task.Task {
if a.taskMgr == nil {
return nil
}
return a.taskMgr.List()
}
// GetTaskLogs returns the stderr logs for a specific task.
func (a *App) GetTaskLogs(taskID string) []string {
if a.taskMgr == nil {
return nil
}
for _, t := range a.taskMgr.List() {
if t.ID == taskID {
return t.Logs
}
}
return nil
}
// SetHWAccel sets the hardware acceleration method for encoding.
func (a *App) SetHWAccel(accel string) {
if a.taskMgr != nil {
a.taskMgr.SetHWAccel(accel)
}
}
// ---- File Dialogs ----
// SelectInputFile opens a file dialog for selecting an input media file.
func (a *App) SelectInputFile() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择输入文件",
Filters: []runtime.FileFilter{
{DisplayName: "视频文件 (*.mp4;*.mkv;*.mov;*.ts;*.avi;*.webm;*.flv)", Pattern: "*.mp4;*.mkv;*.mov;*.ts;*.avi;*.webm;*.flv"},
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
},
})
}
// SelectOutputFile opens a save file dialog.
func (a *App) SelectOutputFile(defaultName string) (string, error) {
return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "选择输出文件",
DefaultFilename: defaultName,
Filters: []runtime.FileFilter{
{DisplayName: "MP4 (*.mp4)", Pattern: "*.mp4"},
{DisplayName: "MKV (*.mkv)", Pattern: "*.mkv"},
{DisplayName: "MOV (*.mov)", Pattern: "*.mov"},
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
},
})
}
// SelectOutputDir opens a directory dialog for selecting a default output folder.
func (a *App) SelectOutputDir() (string, error) {
return runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择默认输出目录",
})
}
// SelectSubtitleFile opens a file dialog for selecting an external subtitle file.
func (a *App) SelectSubtitleFile() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择字幕文件",
Filters: []runtime.FileFilter{
{DisplayName: "字幕文件 (*.srt;*.ass;*.ssa;*.vtt;*.sub)", Pattern: "*.srt;*.ass;*.ssa;*.vtt;*.sub"},
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
},
})
}