mirror of
https://github.com/woodchen-ink/video2gif.git
synced 2025-07-18 05:32:03 +08:00
Update Python version in GitHub Actions workflow to 3.11 and adjust dependencies, including excluding tkinterdnd2 and adding fallback support.
651 lines
23 KiB
Python
651 lines
23 KiB
Python
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
import subprocess
|
||
import sys
|
||
import platform
|
||
import os
|
||
import webbrowser
|
||
from threading import Thread
|
||
import ffmpeg
|
||
import traceback # 用于打印详细错误信息
|
||
|
||
try:
|
||
from tkinterdnd2 import *
|
||
|
||
SUPPORT_DND = True
|
||
except ImportError:
|
||
SUPPORT_DND = False
|
||
print("Warning: tkinterdnd2 not available, drag and drop support will be disabled")
|
||
|
||
|
||
class FFmpegInstaller:
|
||
@staticmethod
|
||
def get_ffmpeg_path():
|
||
"""获取FFmpeg可执行文件路径"""
|
||
try:
|
||
# 获取程序运行路径
|
||
if getattr(sys, "frozen", False):
|
||
# PyInstaller打包后的路径
|
||
base_path = sys._MEIPASS
|
||
else:
|
||
# 开发环境路径
|
||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# FFmpeg路径
|
||
ffmpeg_dir = os.path.join(base_path, "ffmpeg")
|
||
if platform.system().lower() == "windows":
|
||
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg.exe")
|
||
else:
|
||
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg")
|
||
|
||
return ffmpeg_path
|
||
except Exception as e:
|
||
print(f"Error getting FFmpeg path: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def check_ffmpeg():
|
||
"""检查是否可以使用FFmpeg"""
|
||
try:
|
||
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
|
||
if ffmpeg_path and os.path.exists(ffmpeg_path):
|
||
# 使用打包的FFmpeg
|
||
if platform.system().lower() == "windows":
|
||
startupinfo = subprocess.STARTUPINFO()
|
||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||
result = subprocess.run(
|
||
[ffmpeg_path, "-version"],
|
||
capture_output=True,
|
||
startupinfo=startupinfo,
|
||
)
|
||
else:
|
||
result = subprocess.run(
|
||
[ffmpeg_path, "-version"], capture_output=True
|
||
)
|
||
return result.returncode == 0
|
||
|
||
# 尝试系统PATH中的FFmpeg
|
||
if platform.system().lower() == "windows":
|
||
startupinfo = subprocess.STARTUPINFO()
|
||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||
result = subprocess.run(
|
||
["ffmpeg", "-version"], capture_output=True, startupinfo=startupinfo
|
||
)
|
||
else:
|
||
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
|
||
return result.returncode == 0
|
||
|
||
except FileNotFoundError:
|
||
return False
|
||
except Exception as e:
|
||
print(f"FFmpeg检测出错: {str(e)}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_system_info():
|
||
"""获取系统信息"""
|
||
system = platform.system().lower()
|
||
is_64bits = sys.maxsize > 2**32
|
||
return system, is_64bits
|
||
|
||
def show_installation_dialog(self):
|
||
"""显示安装向导对话框"""
|
||
system, is_64bits = self.get_system_info()
|
||
|
||
dialog = tk.Toplevel()
|
||
dialog.title("FFmpeg 安装向导")
|
||
dialog.geometry("500x400")
|
||
dialog.transient() # 设置为模态窗口
|
||
|
||
# 添加说明文本
|
||
ttk.Label(
|
||
dialog,
|
||
text="未检测到 FFmpeg,需要先安装 FFmpeg 才能继续使用。",
|
||
wraplength=450,
|
||
).pack(padx=20, pady=10)
|
||
|
||
if system == "windows":
|
||
self._setup_windows_installer(dialog)
|
||
elif system == "darwin": # macOS
|
||
self._setup_macos_installer(dialog)
|
||
elif system == "linux":
|
||
self._setup_linux_installer(dialog)
|
||
else:
|
||
self._setup_manual_installer(dialog)
|
||
|
||
dialog.focus_set()
|
||
return dialog
|
||
|
||
def _setup_windows_installer(self, dialog):
|
||
"""Windows安装方式"""
|
||
ttk.Label(dialog, text="Windows 安装方式:", font=("", 10, "bold")).pack(
|
||
padx=20, pady=5
|
||
)
|
||
|
||
# 方式1: Chocolatey
|
||
choco_frame = ttk.LabelFrame(dialog, text="方式1:使用 Chocolatey(推荐)")
|
||
choco_frame.pack(padx=20, pady=5, fill="x")
|
||
|
||
ttk.Label(
|
||
choco_frame,
|
||
text="1. 首先安装 Chocolatey,在管理员权限的 PowerShell 中运行:",
|
||
wraplength=450,
|
||
).pack(padx=5, pady=5)
|
||
|
||
choco_cmd = """Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"""
|
||
cmd_text = ttk.Entry(choco_frame, width=50)
|
||
cmd_text.insert(0, choco_cmd)
|
||
cmd_text.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
choco_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(choco_cmd),
|
||
).pack(pady=5)
|
||
|
||
ttk.Label(choco_frame, text="2. 然后安装 FFmpeg,运行:").pack(padx=5, pady=5)
|
||
|
||
ffmpeg_cmd = "choco install ffmpeg"
|
||
cmd_text2 = ttk.Entry(choco_frame, width=50)
|
||
cmd_text2.insert(0, ffmpeg_cmd)
|
||
cmd_text2.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
choco_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
|
||
).pack(pady=5)
|
||
|
||
# 方式2: 直接下载
|
||
direct_frame = ttk.LabelFrame(dialog, text="方式2:直接下载")
|
||
direct_frame.pack(padx=20, pady=5, fill="x")
|
||
|
||
ttk.Label(direct_frame, text="从官方网站下载并手动配置环境变量:").pack(
|
||
padx=5, pady=5
|
||
)
|
||
|
||
ttk.Button(
|
||
direct_frame,
|
||
text="打开下载页面",
|
||
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
|
||
).pack(pady=5)
|
||
|
||
def _setup_macos_installer(self, dialog):
|
||
"""macOS安装方式"""
|
||
ttk.Label(dialog, text="macOS 安装方式:", font=("", 10, "bold")).pack(
|
||
padx=20, pady=5
|
||
)
|
||
|
||
# 方式1: Homebrew
|
||
brew_frame = ttk.LabelFrame(dialog, text="方式1:使用 Homebrew(推荐)")
|
||
brew_frame.pack(padx=20, pady=5, fill="x")
|
||
|
||
ttk.Label(
|
||
brew_frame, text="1. 首先安装 Homebrew,在终端运行:", wraplength=450
|
||
).pack(padx=5, pady=5)
|
||
|
||
brew_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||
cmd_text = ttk.Entry(brew_frame, width=50)
|
||
cmd_text.insert(0, brew_cmd)
|
||
cmd_text.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
brew_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(brew_cmd),
|
||
).pack(pady=5)
|
||
|
||
ttk.Label(brew_frame, text="2. 然后安装 FFmpeg,运行:").pack(padx=5, pady=5)
|
||
|
||
ffmpeg_cmd = "brew install ffmpeg"
|
||
cmd_text2 = ttk.Entry(brew_frame, width=50)
|
||
cmd_text2.insert(0, ffmpeg_cmd)
|
||
cmd_text2.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
brew_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
|
||
).pack(pady=5)
|
||
|
||
def _setup_linux_installer(self, dialog):
|
||
"""Linux安装方式"""
|
||
ttk.Label(dialog, text="Linux 安装方式:", font=("", 10, "bold")).pack(
|
||
padx=20, pady=5
|
||
)
|
||
|
||
# Ubuntu/Debian
|
||
ubuntu_frame = ttk.LabelFrame(dialog, text="Ubuntu/Debian")
|
||
ubuntu_frame.pack(padx=20, pady=5, fill="x")
|
||
|
||
ubuntu_cmd = "sudo apt update && sudo apt install ffmpeg"
|
||
cmd_text = ttk.Entry(ubuntu_frame, width=50)
|
||
cmd_text.insert(0, ubuntu_cmd)
|
||
cmd_text.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
ubuntu_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(ubuntu_cmd),
|
||
).pack(pady=5)
|
||
|
||
# CentOS/RHEL
|
||
centos_frame = ttk.LabelFrame(dialog, text="CentOS/RHEL")
|
||
centos_frame.pack(padx=20, pady=5, fill="x")
|
||
|
||
centos_cmd = "sudo yum install epel-release && sudo yum install ffmpeg"
|
||
cmd_text2 = ttk.Entry(centos_frame, width=50)
|
||
cmd_text2.insert(0, centos_cmd)
|
||
cmd_text2.pack(padx=5, pady=5)
|
||
|
||
ttk.Button(
|
||
centos_frame,
|
||
text="复制命令",
|
||
command=lambda: self._copy_to_clipboard(centos_cmd),
|
||
).pack(pady=5)
|
||
|
||
def _setup_manual_installer(self, dialog):
|
||
"""手动安装说明"""
|
||
ttk.Label(
|
||
dialog, text="请访问 FFmpeg 官方网站下载并安装:", wraplength=450
|
||
).pack(padx=20, pady=10)
|
||
|
||
ttk.Button(
|
||
dialog,
|
||
text="打开 FFmpeg 官方网站",
|
||
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
|
||
).pack(pady=10)
|
||
|
||
def _copy_to_clipboard(self, text):
|
||
"""复制文本到剪贴板"""
|
||
dialog = tk.Toplevel()
|
||
dialog.withdraw()
|
||
dialog.clipboard_clear()
|
||
dialog.clipboard_append(text)
|
||
dialog.update()
|
||
dialog.destroy()
|
||
messagebox.showinfo("提示", "命令已复制到剪贴板!")
|
||
|
||
|
||
class VideoToGifConverter:
|
||
def __init__(self):
|
||
self.check_ffmpeg_installation()
|
||
|
||
# 根据是否支持拖放来创建不同的根窗口
|
||
if SUPPORT_DND:
|
||
self.root = TkinterDnD.Tk()
|
||
else:
|
||
self.root = tk.Tk()
|
||
|
||
self.root.title("视频转GIF工具")
|
||
|
||
# 只在支持拖放时启用相关功能
|
||
if SUPPORT_DND:
|
||
self.root.drop_target_register(DND_FILES)
|
||
self.root.dnd_bind("<<Drop>>", self.handle_drop)
|
||
|
||
self.setup_ui()
|
||
|
||
# 添加拖放处理方法
|
||
def handle_drop(self, event):
|
||
"""处理文件拖放"""
|
||
files = self.root.tk.splitlist(event.data)
|
||
# 过滤出视频文件
|
||
valid_extensions = (".mp4", ".avi", ".mov", ".mkv")
|
||
valid_files = [f for f in files if f.lower().endswith(valid_extensions)]
|
||
|
||
if valid_files:
|
||
self.files_list.delete(0, tk.END)
|
||
for file in valid_files:
|
||
self.files_list.insert(tk.END, file)
|
||
else:
|
||
messagebox.showwarning(
|
||
"警告", "请拖入视频文件(支持mp4, avi, mov, mkv格式)"
|
||
)
|
||
|
||
def check_ffmpeg_installation(self):
|
||
"""检查FFmpeg是否已安装"""
|
||
installer = FFmpegInstaller()
|
||
if not installer.check_ffmpeg():
|
||
# 创建一个临时的root窗口来显示消息框
|
||
temp_root = tk.Tk()
|
||
temp_root.withdraw() # 隐藏临时窗口
|
||
|
||
response = messagebox.askquestion(
|
||
"FFmpeg未安装", "需要安装FFmpeg才能使用本工具。是否查看安装指南?"
|
||
)
|
||
|
||
if response == "yes":
|
||
dialog = installer.show_installation_dialog()
|
||
temp_root.wait_window(dialog) # 等待安装向导窗口关闭
|
||
|
||
# 再次检查安装
|
||
if not installer.check_ffmpeg():
|
||
if (
|
||
messagebox.askquestion(
|
||
"提示", "请安装完FFmpeg后再运行程序。是否退出程序?"
|
||
)
|
||
== "yes"
|
||
):
|
||
temp_root.destroy()
|
||
sys.exit()
|
||
else:
|
||
temp_root.destroy()
|
||
sys.exit()
|
||
|
||
temp_root.destroy()
|
||
|
||
def setup_ui(self):
|
||
# 文件选择框
|
||
self.file_frame = ttk.LabelFrame(self.root, text="选择文件")
|
||
self.file_frame.pack(padx=10, pady=5, fill="x")
|
||
|
||
self.files_list = tk.Listbox(self.file_frame, height=5)
|
||
self.files_list.pack(padx=5, pady=5, fill="x")
|
||
|
||
btn_frame = ttk.Frame(self.file_frame)
|
||
btn_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
self.select_btn = ttk.Button(
|
||
btn_frame, text="选择视频", command=self.select_files
|
||
)
|
||
self.select_btn.pack(side="left", padx=5)
|
||
|
||
self.clear_btn = ttk.Button(
|
||
btn_frame,
|
||
text="清空列表",
|
||
command=lambda: self.files_list.delete(0, tk.END),
|
||
)
|
||
self.clear_btn.pack(side="left", padx=5)
|
||
|
||
# 转换设置框
|
||
self.settings_frame = ttk.LabelFrame(self.root, text="转换设置")
|
||
self.settings_frame.pack(padx=10, pady=5, fill="x")
|
||
|
||
# 尺寸设置
|
||
size_frame = ttk.Frame(self.settings_frame)
|
||
size_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(size_frame, text="尺寸设置:").pack(side="left", padx=5)
|
||
self.size_var = tk.StringVar(value="original")
|
||
ttk.Radiobutton(
|
||
size_frame, text="保持原始尺寸", variable=self.size_var, value="original"
|
||
).pack(side="left", padx=5)
|
||
ttk.Radiobutton(
|
||
size_frame, text="自定义尺寸", variable=self.size_var, value="custom"
|
||
).pack(side="left", padx=5)
|
||
|
||
# 自定义尺寸输入框
|
||
custom_size_frame = ttk.Frame(self.settings_frame)
|
||
custom_size_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(custom_size_frame, text="宽度:").pack(side="left", padx=5)
|
||
self.width_var = tk.StringVar(value="480")
|
||
self.width_entry = ttk.Entry(
|
||
custom_size_frame, textvariable=self.width_var, width=8
|
||
)
|
||
self.width_entry.pack(side="left", padx=5)
|
||
|
||
ttk.Label(custom_size_frame, text="高度:").pack(side="left", padx=5)
|
||
self.height_var = tk.StringVar(value="auto")
|
||
self.height_entry = ttk.Entry(
|
||
custom_size_frame, textvariable=self.height_var, width=8
|
||
)
|
||
self.height_entry.pack(side="left", padx=5)
|
||
|
||
# FPS设置
|
||
fps_frame = ttk.Frame(self.settings_frame)
|
||
fps_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(fps_frame, text="FPS:").pack(side="left", padx=5)
|
||
self.fps_var = tk.StringVar(value="10")
|
||
self.fps_entry = ttk.Entry(fps_frame, textvariable=self.fps_var, width=8)
|
||
self.fps_entry.pack(side="left", padx=5)
|
||
|
||
# 时长控制
|
||
duration_frame = ttk.Frame(self.settings_frame)
|
||
duration_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(duration_frame, text="时长控制(秒):").pack(side="left", padx=5)
|
||
ttk.Label(duration_frame, text="开始时间:").pack(side="left", padx=5)
|
||
self.start_time_var = tk.StringVar(value="0")
|
||
self.start_time_entry = ttk.Entry(
|
||
duration_frame, textvariable=self.start_time_var, width=8
|
||
)
|
||
self.start_time_entry.pack(side="left", padx=5)
|
||
|
||
ttk.Label(duration_frame, text="持续时间:").pack(side="left", padx=5)
|
||
self.duration_var = tk.StringVar(value="")
|
||
self.duration_entry = ttk.Entry(
|
||
duration_frame, textvariable=self.duration_var, width=8
|
||
)
|
||
self.duration_entry.pack(side="left", padx=5)
|
||
ttk.Label(duration_frame, text="(留空表示全部)").pack(side="left", padx=5)
|
||
|
||
# 质量设置
|
||
quality_frame = ttk.Frame(self.settings_frame)
|
||
quality_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(quality_frame, text="质量设置:").pack(side="left", padx=5)
|
||
self.quality_var = tk.StringVar(value="medium")
|
||
ttk.Radiobutton(
|
||
quality_frame, text="高质量", variable=self.quality_var, value="high"
|
||
).pack(side="left", padx=5)
|
||
ttk.Radiobutton(
|
||
quality_frame, text="中等", variable=self.quality_var, value="medium"
|
||
).pack(side="left", padx=5)
|
||
ttk.Radiobutton(
|
||
quality_frame, text="低质量", variable=self.quality_var, value="low"
|
||
).pack(side="left", padx=5)
|
||
|
||
# 输出设置
|
||
output_frame = ttk.Frame(self.settings_frame)
|
||
output_frame.pack(fill="x", padx=5, pady=5)
|
||
|
||
ttk.Label(output_frame, text="输出位置:").pack(side="left", padx=5)
|
||
self.output_var = tk.StringVar(value="same")
|
||
ttk.Radiobutton(
|
||
output_frame, text="与视频相同目录", variable=self.output_var, value="same"
|
||
).pack(side="left", padx=5)
|
||
ttk.Radiobutton(
|
||
output_frame, text="自定义目录", variable=self.output_var, value="custom"
|
||
).pack(side="left", padx=5)
|
||
|
||
self.output_path_var = tk.StringVar()
|
||
self.output_path_entry = ttk.Entry(
|
||
output_frame, textvariable=self.output_path_var, width=30
|
||
)
|
||
self.output_path_entry.pack(side="left", padx=5)
|
||
|
||
self.browse_btn = ttk.Button(
|
||
output_frame, text="浏览", command=self.browse_output
|
||
)
|
||
self.browse_btn.pack(side="left", padx=5)
|
||
|
||
# 转换按钮
|
||
self.convert_btn = ttk.Button(
|
||
self.root, text="开始转换", command=self.start_conversion
|
||
)
|
||
self.convert_btn.pack(pady=10)
|
||
|
||
# 进度条
|
||
self.progress = ttk.Progressbar(self.root, mode="determinate")
|
||
self.progress.pack(fill="x", padx=10, pady=5)
|
||
|
||
# 状态标签
|
||
self.status_label = ttk.Label(self.root, text="就绪")
|
||
self.status_label.pack(pady=5)
|
||
# 如果不支持拖放,添加提示
|
||
if not SUPPORT_DND:
|
||
ttk.Label(
|
||
self.file_frame,
|
||
text="注意:当前版本不支持拖放功能,请使用'选择视频'按钮",
|
||
wraplength=300,
|
||
foreground="red",
|
||
).pack(pady=5)
|
||
|
||
def browse_output(self):
|
||
directory = filedialog.askdirectory()
|
||
if directory:
|
||
self.output_path_var.set(directory)
|
||
|
||
def select_files(self):
|
||
files = filedialog.askopenfilenames(
|
||
filetypes=[("Video files", "*.mp4 *.avi *.mov *.mkv")]
|
||
)
|
||
self.files_list.delete(0, tk.END)
|
||
for file in files:
|
||
self.files_list.insert(tk.END, file)
|
||
|
||
def get_quality_settings(self):
|
||
"""根据质量设置返回 FFmpeg 参数"""
|
||
quality = self.quality_var.get()
|
||
if quality == "high":
|
||
return ["-quality", "100"]
|
||
elif quality == "medium":
|
||
return ["-quality", "75"]
|
||
else:
|
||
return ["-quality", "50"]
|
||
|
||
# 修改 convert_video_to_gif 方法
|
||
def convert_video_to_gif(self, video_path):
|
||
try:
|
||
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
|
||
if ffmpeg_path:
|
||
# 设置FFmpeg路径
|
||
ffmpeg.input.DEFAULT_FFMPEG_PATH = ffmpeg_path
|
||
# 确定输出路径
|
||
if self.output_var.get() == "same":
|
||
output_dir = os.path.dirname(video_path)
|
||
else:
|
||
output_dir = self.output_path_var.get()
|
||
if not output_dir:
|
||
raise ValueError("请选择输出目录")
|
||
|
||
output_path = os.path.join(
|
||
output_dir, os.path.splitext(os.path.basename(video_path))[0] + ".gif"
|
||
)
|
||
|
||
# 获取设置值
|
||
start_time = float(self.start_time_var.get() or 0)
|
||
duration = self.duration_var.get()
|
||
fps = int(self.fps_var.get())
|
||
|
||
# 设置调色板生成
|
||
palette_path = os.path.join(output_dir, "palette.png")
|
||
|
||
# 获取CPU核心数
|
||
cpu_count = os.cpu_count() or 1
|
||
threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用
|
||
|
||
# 第一步:生成调色板(添加线程参数)
|
||
stream = ffmpeg.input(video_path)
|
||
|
||
if start_time > 0:
|
||
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
|
||
|
||
if duration:
|
||
stream = ffmpeg.filter(stream, "t", duration=float(duration))
|
||
|
||
stream = ffmpeg.filter(stream, "fps", fps=fps)
|
||
|
||
if self.size_var.get() == "custom":
|
||
width = int(self.width_var.get())
|
||
height = self.height_var.get()
|
||
height = -1 if height == "auto" else int(height)
|
||
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
|
||
|
||
stream = ffmpeg.filter(stream, "palettegen")
|
||
# 添加线程参数
|
||
ffmpeg.run(
|
||
ffmpeg.output(stream, palette_path, threads=threads),
|
||
overwrite_output=True,
|
||
)
|
||
|
||
# 第二步:使用调色板生成GIF(添加线程参数)
|
||
stream = ffmpeg.input(video_path)
|
||
palette = ffmpeg.input(palette_path)
|
||
|
||
if start_time > 0:
|
||
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
|
||
|
||
if duration:
|
||
stream = ffmpeg.filter(stream, "t", duration=float(duration))
|
||
|
||
stream = ffmpeg.filter(stream, "fps", fps=fps)
|
||
|
||
if self.size_var.get() == "custom":
|
||
width = int(self.width_var.get())
|
||
height = self.height_var.get()
|
||
height = -1 if height == "auto" else int(height)
|
||
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
|
||
|
||
stream = ffmpeg.filter([stream, palette], "paletteuse")
|
||
# 添加线程参数
|
||
ffmpeg.run(
|
||
ffmpeg.output(stream, output_path, threads=threads),
|
||
overwrite_output=True,
|
||
)
|
||
|
||
# 删除临时调色板文件
|
||
if os.path.exists(palette_path):
|
||
os.remove(palette_path)
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
print(f"Conversion error: {error_msg}")
|
||
messagebox.showerror("错误", f"转换失败:\n{error_msg}")
|
||
return False
|
||
|
||
def start_conversion(self):
|
||
def convert():
|
||
files = self.files_list.get(0, tk.END)
|
||
if not files:
|
||
messagebox.showwarning("警告", "请先选择要转换的视频文件")
|
||
return
|
||
|
||
total = len(files)
|
||
|
||
for i, file in enumerate(files):
|
||
current = i + 1
|
||
self.status_label.config(
|
||
text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
|
||
)
|
||
success = self.convert_video_to_gif(file)
|
||
progress = current / total * 100
|
||
self.progress["value"] = progress
|
||
|
||
self.status_label.config(text=f"转换完成 ({total}/{total})")
|
||
self.convert_btn["state"] = "normal"
|
||
messagebox.showinfo("完成", f"所有文件转换完成!\n成功转换 {total} 个文件")
|
||
|
||
self.convert_btn["state"] = "disabled"
|
||
self.progress["value"] = 0
|
||
Thread(target=convert).start()
|
||
|
||
def run(self):
|
||
self.root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
print("程序启动...")
|
||
# 先单独测试 FFmpeg
|
||
installer = FFmpegInstaller()
|
||
ffmpeg_installed = installer.check_ffmpeg()
|
||
print(f"FFmpeg 检测结果: {ffmpeg_installed}")
|
||
|
||
if not ffmpeg_installed:
|
||
print("FFmpeg 未安装,将显示安装向导...")
|
||
|
||
app = VideoToGifConverter()
|
||
app.run()
|
||
except Exception as e:
|
||
print(f"程序运行出错: {str(e)}")
|
||
import traceback
|
||
|
||
traceback.print_exc() # 打印详细错误信息
|
||
sys.exit(1)
|