import subprocess
import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import re
class VideoConverter:
def __init__(self, ffmpeg_path):
self.ffmpeg_path = ffmpeg_path
def convert_video(self, input_file, output_file, codec="libx264", quality="medium", progress_callback=None):
"""转换单个视频文件"""
try:
cmd = [
self.ffmpeg_path,
"-i", input_file,
"-c:v", codec,
"-preset", quality,
"-crf", "23",
"-c:a", "aac",
"-y",
output_file
]
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
total_duration = 0
current_time = 0
for line in process.stdout:
duration_match = re.search(r"Duration: (\d+:\d+:\d+\.\d+)", line)
if duration_match and total_duration == 0:
duration_str = duration_match.group(1)
h, m, s = map(float, duration_str.split(':'))
total_duration = h * 3600 + m * 60 + s
time_match = re.search(r"time=(\d+:\d+:\d+\.\d+)", line)
if time_match and total_duration > 0:
time_str = time_match.group(1)
h, m, s = map(float, time_str.split(':'))
current_time = h * 3600 + m * 60 + s
progress = int((current_time / total_duration) * 100)
progress = min(progress, 100)
if progress_callback:
progress_callback(progress)
process.wait()
if process.returncode != 0:
raise Exception(f"FFmpeg执行失败,返回代码: {process.returncode}")
if not os.path.exists(output_file) or os.path.getsize(output_file) == 0:
raise Exception("转换后的文件为空或不存在")
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
def batch_convert(self, input_files, output_dir, naming_rule, output_format, codec="libx264", quality="medium",
progress_callback=None):
"""批量转换视频文件"""
results = []
total_files = len(input_files)
for i, input_file in enumerate(input_files):
output_file = self.generate_output_filename(
input_file,
output_dir,
naming_rule,
output_format
)
success = self.convert_video(
input_file,
output_file,
codec=codec,
quality=quality,
progress_callback=lambda p: progress_callback(i, total_files, p) if progress_callback else None
)
results.append((input_file, output_file, success))
return results
def generate_output_filename(self, input_file, output_dir, naming_rule, output_format):
"""根据命名规则生成输出文件名"""
base_name = os.path.basename(input_file)
file_name, _ = os.path.splitext(base_name)
rule_type = naming_rule["type"]
if rule_type == "original":
output_name = f"{file_name}.{output_format}"
elif rule_type == "prefix":
prefix = naming_rule.get("prefix", "")
output_name = f"{prefix}{file_name}.{output_format}"
elif rule_type == "suffix":
suffix = naming_rule.get("suffix", "")
output_name = f"{file_name}{suffix}.{output_format}"
elif rule_type == "custom":
custom_format = naming_rule.get("format", "{original}")
output_name = custom_format.replace("{original}", file_name) + f".{output_format}"
else:
output_name = f"{file_name}.{output_format}"
output_path = os.path.join(output_dir, output_name)
counter = 1
while os.path.exists(output_path):
base, ext = os.path.splitext(output_name)
output_name = f"{base}_{counter}{ext}"
output_path = os.path.join(output_dir, output_name)
counter += 1
return output_path
class VideoConverterGUI:
def __init__(self, root):
self.root = root
self.root.title("视频格式转换工具")
self.root.geometry("750x620")
self.root.resizable(True, True)
self.ffmpeg_path = ""
self.converter = None
self.input_files = []
self.output_dir = ""
self.output_format = "mp4"
self.codec = "libx264"
self.quality = "medium"
self.naming_rule = {"type": "original"}
self.create_widgets()
def create_widgets(self):
"""创建GUI界面组件"""
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
ffmpeg_frame = ttk.LabelFrame(main_frame, text="FFmpeg设置", padding="10")
ffmpeg_frame.pack(fill=tk.X, pady=5)
ttk.Button(ffmpeg_frame, text="选择ffmpeg.exe", command=self.select_ffmpeg).pack(side=tk.LEFT, padx=5)
self.ffmpeg_path_var = tk.StringVar()
self.ffmpeg_path_var.set("未选择ffmpeg.exe文件")
ttk.Label(ffmpeg_frame, textvariable=self.ffmpeg_path_var).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="10")
file_frame.pack(fill=tk.X, pady=5)
ttk.Button(file_frame, text="添加文件", command=self.select_files).pack(side=tk.LEFT, padx=5)
ttk.Button(file_frame, text="添加文件夹", command=self.select_directory).pack(side=tk.LEFT, padx=5)
ttk.Button(file_frame, text="清空列表", command=self.clear_files).pack(side=tk.LEFT, padx=5)
list_frame = ttk.LabelFrame(main_frame, text="文件列表", padding="10")
list_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.file_listbox = tk.Listbox(list_frame, height=8, selectmode=tk.EXTENDED)
self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.file_listbox.config(yscrollcommand=scrollbar.set)
output_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="10")
output_frame.pack(fill=tk.X, pady=5)
ttk.Button(output_frame, text="选择输出文件夹", command=self.select_output_dir).pack(side=tk.LEFT, padx=5)
self.output_dir_var = tk.StringVar()
ttk.Label(output_frame, textvariable=self.output_dir_var).pack(side=tk.LEFT, padx=5)
settings_frame = ttk.LabelFrame(main_frame, text="转换设置", padding="10")
settings_frame.pack(fill=tk.X, pady=5)
ttk.Label(settings_frame, text="输出格式:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
format_values = ["mp4", "avi", "mkv", "mov", "wmv", "flv", "webm", "3gp", "m4v"]
self.format_combo = ttk.Combobox(settings_frame, values=format_values, width=10)
self.format_combo.set(self.output_format)
self.format_combo.grid(row=0, column=1, padx=5, pady=5)
self.format_combo.bind("<>", self.on_format_changed)
ttk.Label(settings_frame, text="编码格式:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
codec_values = [
"libx264",
"libx265",
"mpeg4",
"libvpx-vp9",
"libvpx",
"flv",
"h263",
"mpeg2video"
]
self.codec_combo = ttk.Combobox(settings_frame, values=codec_values, width=12)
self.codec_combo.set(self.codec)
self.codec_combo.grid(row=0, column=3, padx=5, pady=5)
ttk.Label(settings_frame, text="质量预设:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
quality_values = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"]
self.quality_combo = ttk.Combobox(settings_frame, values=quality_values, width=10)
self.quality_combo.set(self.quality)
self.quality_combo.grid(row=0, column=5, padx=5, pady=5)
ttk.Label(settings_frame, text="文件名规则:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
naming_values = ["保持原文件名", "添加前缀", "添加后缀", "自定义格式"]
self.naming_combo = ttk.Combobox(settings_frame, values=naming_values, width=15)
self.naming_combo.set(naming_values[0])
self.naming_combo.grid(row=1, column=1, padx=5, pady=5)
self.naming_combo.bind("<>", self.on_naming_rule_changed)
self.naming_entry = ttk.Entry(settings_frame, width=30)
self.naming_entry.grid(row=1, column=2, columnspan=3, padx=5, pady=5, sticky=tk.W)
self.naming_entry.insert(0, "输入前缀/后缀/自定义格式")
self.naming_entry.config(state=tk.DISABLED)
ttk.Label(settings_frame, text="(支持{original}占位符)").grid(row=1, column=5, padx=5, pady=5, sticky=tk.W)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill=tk.X, pady=5)
self.status_var = tk.StringVar()
self.status_var.set("就绪")
ttk.Label(main_frame, textvariable=self.status_var).pack(anchor=tk.W, pady=5)
ttk.Button(main_frame, text="开始转换", command=self.start_conversion).pack(pady=10)
def on_format_changed(self, event):
"""当输出格式改变时,自动推荐匹配的编码器"""
selected_format = self.format_combo.get()
format_codec_map = {
"mp4": "libx264",
"avi": "mpeg4",
"mkv": "libx264",
"mov": "libx264",
"wmv": "mpeg4",
"flv": "flv",
"webm": "libvpx-vp9",
"3gp": "h263",
"m4v": "libx264"
}
if selected_format in format_codec_map:
self.codec_combo.set(format_codec_map[selected_format])
def select_ffmpeg(self):
"""选择ffmpeg.exe文件"""
ffmpeg_file = filedialog.askopenfilename(
title="选择ffmpeg.exe",
filetypes=[("可执行文件", "ffmpeg.exe")]
)
if ffmpeg_file:
self.ffmpeg_path = ffmpeg_file
self.ffmpeg_path_var.set(ffmpeg_file)
self.converter = VideoConverter(ffmpeg_file)
def select_files(self):
"""选择多个视频文件(包含新增格式)"""
files = filedialog.askopenfilenames(
title="选择视频文件",
filetypes=[("视频文件", "*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.3gp *.m4v")]
)
if files:
self.input_files.extend(files)
self.update_file_listbox()
def select_directory(self):
"""选择包含视频文件的文件夹(支持新增格式)"""
directory = filedialog.askdirectory(title="选择文件夹")
if directory:
video_extensions = [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".3gp", ".m4v"]
files = [
os.path.join(directory, f)
for f in os.listdir(directory)
if os.path.isfile(os.path.join(directory, f))
and os.path.splitext(f)[1].lower() in video_extensions
]
if files:
self.input_files.extend(files)
self.update_file_listbox()
else:
messagebox.showinfo("提示", "所选文件夹中没有找到支持的视频文件")
def clear_files(self):
"""清空文件列表"""
self.input_files = []
self.update_file_listbox()
def update_file_listbox(self):
"""更新文件列表显示"""
self.file_listbox.delete(0, tk.END)
for file_path in self.input_files:
self.file_listbox.insert(tk.END, os.path.basename(file_path))
def select_output_dir(self):
"""选择输出文件夹"""
directory = filedialog.askdirectory(title="选择输出文件夹")
if directory:
self.output_dir = directory
self.output_dir_var.set(directory)
def on_naming_rule_changed(self, event):
"""当命名规则选择变化时更新输入框状态"""
selection = self.naming_combo.get()
if selection == "保持原文件名":
self.naming_entry.config(state=tk.DISABLED)
self.naming_entry.delete(0, tk.END)
self.naming_entry.insert(0, "输入前缀/后缀/自定义格式")
else:
self.naming_entry.config(state=tk.NORMAL)
if selection == "添加前缀" and self.naming_entry.get() == "输入前缀/后缀/自定义格式":
self.naming_entry.delete(0, tk.END)
self.naming_entry.insert(0, "new_")
elif selection == "添加后缀" and self.naming_entry.get() == "输入前缀/后缀/自定义格式":
self.naming_entry.delete(0, tk.END)
self.naming_entry.insert(0, "_converted")
elif selection == "自定义格式" and self.naming_entry.get() == "输入前缀/后缀/自定义格式":
self.naming_entry.delete(0, tk.END)
self.naming_entry.insert(0, "{original}_new")
def update_progress(self, current_file, total_files, progress):
"""更新进度条和状态文本"""
overall_progress = ((current_file / total_files) + (progress / 100 / total_files)) * 100
self.progress_var.set(overall_progress)
if progress < 100:
self.status_var.set(f"正在转换文件 {current_file + 1}/{total_files}: {progress}%")
else:
self.status_var.set(f"文件 {current_file + 1}/{total_files} 转换完成")
def conversion_complete(self, results):
"""转换完成后的处理"""
self.progress_var.set(100)
success_count = sum(1 for _, _, success in results if success)
failed_count = len(results) - success_count
message = f"转换完成!\n成功: {success_count}\n失败: {failed_count}"
if failed_count > 0:
failed_files = [os.path.basename(f) for f, _, s in results if not s]
message += f"\n\n失败的文件:\n{', '.join(failed_files)}"
messagebox.showinfo("转换结果", message)
self.status_var.set("就绪")
def start_conversion(self):
"""开始转换过程(增加格式与编码兼容性检查)"""
if not self.ffmpeg_path or not self.converter:
messagebox.showwarning("警告", "请先选择有效的ffmpeg.exe文件")
return
if not self.input_files:
messagebox.showwarning("警告", "请先选择要转换的视频文件")
return
if not self.output_dir:
messagebox.showwarning("警告", "请先选择输出文件夹")
return
self.output_format = self.format_combo.get()
self.codec = self.codec_combo.get()
self.quality = self.quality_combo.get()
naming_selection = self.naming_combo.get()
naming_text = self.naming_entry.get()
if naming_selection == "保持原文件名":
self.naming_rule = {"type": "original"}
elif naming_selection == "添加前缀":
self.naming_rule = {"type": "prefix", "prefix": naming_text}
elif naming_selection == "添加后缀":
self.naming_rule = {"type": "suffix", "suffix": naming_text}
elif naming_selection == "自定义格式":
self.naming_rule = {"type": "custom", "format": naming_text}
thread = threading.Thread(
target=self.perform_conversion,
daemon=True
)
thread.start()
def perform_conversion(self):
"""执行实际的转换过程"""
self.status_var.set("开始转换...")
results = self.converter.batch_convert(
self.input_files,
self.output_dir,
self.naming_rule,
self.output_format,
codec=self.codec,
quality=self.quality,
progress_callback=self.update_progress
)
self.root.after(0, lambda: self.conversion_complete(results))
def main():
root = tk.Tk()
app = VideoConverterGUI(root)
root.mainloop()
if __name__ == "__main__":
main()