#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 视频格式转换工具(带GUI界面) 使用ffmpeg将指定目录下的全部视频转换为mp4格式的横屏1080p视频 支持配置ffmpeg的大部分参数 """ import os import subprocess import sys import threading import tkinter as tk from tkinter import filedialog, messagebox, ttk from concurrent.futures import ThreadPoolExecutor, as_completed class VideoConverterUI: def __init__(self, root): self.root = root self.root.title("视频格式转换工具") self.root.geometry("800x800") self.root.resizable(True, True) # 设置主题颜色 self.bg_color = "#f0f0f0" # 主窗口背景色(浅灰) self.accent_color = "#4CAF50" # 强调色:标题、悬停边框(绿色) self.text_color = "#000000" # 普通文字颜色(黑色) self.frame_color = "#ffffff" # 内部框架背景色(白色) self.button_bg = "#4CAF50" # 按钮背景色(绿色) self.button_fg = "#000000" # 按钮文字颜色(白色) # 变量 self.input_dir = tk.StringVar() self.output_dir = tk.StringVar(value="output") self.threads = tk.StringVar(value="4") self.ffmpeg_params = tk.StringVar() self.is_converting = False self.convert_thread = None # 创建界面 self.create_widgets() def create_widgets(self): # 设置窗口背景 self.root.configure(bg=self.bg_color) # 主框架 main_frame = ttk.Frame(self.root, padding="20", style="Main.TFrame") main_frame.pack(fill=tk.BOTH, expand=True) # 设置样式 self.setup_styles() # 标题 title_label = ttk.Label(main_frame, text="视频格式转换工具", font=("Microsoft YaHei", 18, "bold"), style="Title.TLabel") title_label.pack(pady=10) # 输入目录 input_frame = ttk.LabelFrame(main_frame, text="输入设置", padding="15") input_frame.pack(fill=tk.X, pady=10) # 输入目录行 input_row = ttk.Frame(input_frame) input_row.pack(fill=tk.X, pady=8) ttk.Label(input_row, text="输入目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) # 创建可拖拽的输入框 self.input_entry = ttk.Entry(input_row, textvariable=self.input_dir, width=60, style="TEntry") self.input_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) # 启用拖放功能 self.input_entry.bind("", self.clear_placeholder) self.input_entry.bind("", self.on_enter) self.input_entry.bind("", self.on_leave) # 为输入框添加拖放支持 self.input_entry.bind("", self.on_right_click) # 为窗口添加拖放支持(使用tkinter标准事件) self.root.bind("", self.on_button_press) self.root.bind("", self.on_mouse_move) self.root.bind("", self.on_button_release) # 为输入框添加粘贴支持 self.input_entry.bind("", self.on_paste) self.input_entry.bind("", self.on_paste) ttk.Button(input_row, text="浏览", command=self.browse_input, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5) # 输出目录行 output_row = ttk.Frame(input_frame) output_row.pack(fill=tk.X, pady=8) ttk.Label(output_row, text="输出目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) ttk.Entry(output_row, textvariable=self.output_dir, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) ttk.Button(output_row, text="浏览", command=self.browse_output, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5) # 线程数和参数行 settings_row = ttk.Frame(input_frame) settings_row.pack(fill=tk.X, pady=8) threads_frame = ttk.Frame(settings_row) threads_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) ttk.Label(threads_frame, text="并发线程数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) ttk.Entry(threads_frame, textvariable=self.threads, width=10, style="TEntry").pack(side=tk.LEFT, padx=5) # 分辨率设置按钮 resolution_frame = ttk.LabelFrame(main_frame, text="分辨率设置", padding="15") resolution_frame.pack(fill=tk.X, pady=10) buttons_frame = ttk.Frame(resolution_frame) buttons_frame.pack(fill=tk.X, pady=5) ttk.Button(buttons_frame, text="强制高度为1080", command=self.set_height_1080, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) ttk.Button(buttons_frame, text="强制宽度为1920", command=self.set_width_1920, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) ttk.Button(buttons_frame, text="严格1080p(首选)", command=self.set_strict_1080p, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) # FFmpeg参数设置 params_frame = ttk.LabelFrame(main_frame, text="FFmpeg参数设置", padding="15") params_frame.pack(fill=tk.X, pady=10) params_row = ttk.Frame(params_frame) params_row.pack(fill=tk.X, pady=5) ttk.Label(params_row, text="额外参数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) ttk.Entry(params_row, textvariable=self.ffmpeg_params, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) # 控制按钮 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=15) button_container = ttk.Frame(button_frame) button_container.pack(side=tk.LEFT, padx=5) self.convert_button = ttk.Button(button_container, text="开始转换", command=self.start_conversion, style="Start.TButton") self.convert_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25) self.stop_button = ttk.Button(button_container, text="停止转换", command=self.stop_conversion, state=tk.DISABLED, style="Stop.TButton") self.stop_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25) # 状态框 status_frame = ttk.LabelFrame(main_frame, text="转换状态", padding="15") status_frame.pack(fill=tk.BOTH, expand=True, pady=10) # 状态文本框 self.status_text = tk.Text(status_frame, wrap=tk.WORD, height=15, bg="#f8f8f8", fg=self.text_color, font=("Microsoft YaHei", 10)) self.status_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=5, pady=5) # 滚动条 scrollbar = ttk.Scrollbar(status_frame, command=self.status_text.yview, style="TScrollbar") scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5) self.status_text.config(yscrollcommand=scrollbar.set) # 底部状态 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel") status_bar.pack(fill=tk.X, side=tk.BOTTOM) def setup_styles(self): """设置界面样式""" style = ttk.Style() # 主框架样式 style.configure("Main.TFrame", background=self.bg_color) # 标题样式 style.configure("Title.TLabel", font=("Microsoft YaHei", 18, "bold"), foreground=self.accent_color, background=self.bg_color) # 标签样式 style.configure("TLabel", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color, background=self.frame_color) # 输入框样式 style.configure("TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.text_color) # 悬停输入框样式 style.configure("Hover.TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.accent_color, background="#f0f8f0") # 按钮样式 style.configure("Accent.TButton", font=("Microsoft YaHei", 10), padding=5, background=self.button_bg, foreground=self.button_fg) style.map("Accent.TButton", background=[("active", "#45a049")]) # 开始按钮样式 style.configure("Start.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background=self.button_bg, foreground=self.button_fg) style.map("Start.TButton", background=[("active", "#45a049")]) # 停止按钮样式 style.configure("Stop.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background="#f44336", foreground="#ffffff") style.map("Stop.TButton", background=[("active", "#da190b")]) # 标签框架样式 style.configure("TLabelFrame", font=("Microsoft YaHei", 10, "bold"), background=self.bg_color, foreground=self.text_color) style.configure("TLabelFrame.Label", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color) # 滚动条样式 style.configure("TScrollbar", background=self.bg_color, troughcolor="#e0e0e0") # 状态标签样式 style.configure("Status.TLabel", font=("Microsoft YaHei", 9), padding=5, background="#e0e0e0", foreground=self.text_color) def browse_input(self): dir_path = filedialog.askdirectory(title="选择输入目录") if dir_path: self.input_dir.set(dir_path) def browse_output(self): dir_path = filedialog.askdirectory(title="选择输出目录") if dir_path: self.output_dir.set(dir_path) def set_height_1080(self): """设置高度为1080""" current_params = self.ffmpeg_params.get().strip() # 移除现有的高度设置 new_params = [] for param in current_params.split(): if not (param.startswith('-h') and param != '-h'): new_params.append(param) # 添加新的高度设置 new_params.extend(['-vf', 'scale=-2:1080']) self.ffmpeg_params.set(' '.join(new_params)) self.log("已设置高度为1080") def set_width_1920(self): """设置宽度为1920""" current_params = self.ffmpeg_params.get().strip() # 移除现有的宽度设置 new_params = [] for param in current_params.split(): if not (param.startswith('-w') and param != '-w'): new_params.append(param) # 添加新的宽度设置 new_params.extend(['-vf', 'scale=1920:-2']) self.ffmpeg_params.set(' '.join(new_params)) self.log("已设置宽度为1920") def set_strict_1080p(self): """设置严格的1080p分辨率""" current_params = self.ffmpeg_params.get().strip() # 移除现有的分辨率设置 new_params = [] skip_next = False for i, param in enumerate(current_params.split()): if skip_next: skip_next = False continue if param == '-vf' and i + 1 < len(current_params.split()): if 'scale=' in current_params.split()[i + 1]: skip_next = True continue new_params.append(param) # 添加严格的1080p设置(先缩放保持比例,再用pad填充黑边至标准1080p) new_params.extend(['-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2']) self.ffmpeg_params.set(' '.join(new_params)) self.log("已设置严格的1080p分辨率(保持比例+黑边填充)") def clear_placeholder(self, event): """清除输入框的占位符""" pass def on_enter(self, event): """鼠标进入输入框""" self.input_entry.configure(style="Hover.TEntry") def on_leave(self, event): """鼠标离开输入框""" self.input_entry.configure(style="TEntry") def on_button_press(self, event): """鼠标按下""" pass def on_mouse_move(self, event): """鼠标移动""" pass def on_button_release(self, event): """鼠标释放""" pass def on_right_click(self, event): """右键点击""" pass def on_paste(self, event): """粘贴功能""" try: # 获取剪贴板内容 clipboard_content = self.root.clipboard_get() if clipboard_content: # 检查是否是有效的文件或目录路径 if os.path.exists(clipboard_content): # 如果是文件,使用其所在目录 if os.path.isfile(clipboard_content): input_path = os.path.dirname(clipboard_content) else: input_path = clipboard_content self.input_dir.set(input_path) self.log(f"已添加输入目录: {input_path}") except: pass def log(self, message): """记录日志到状态文本框""" self.status_text.insert(tk.END, message + "\n") self.status_text.see(tk.END) self.root.update_idletasks() def get_video_files(self, directory): """ 获取目录下所有视频文件 """ video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm', '.m4v'] video_files = [] for root, _, files in os.walk(directory): for file in files: if any(file.lower().endswith(ext) for ext in video_extensions): video_files.append(os.path.join(root, file)) return video_files def convert_video(self, input_file, output_file, ffmpeg_args): """ 转换单个视频文件 """ try: # 构建ffmpeg命令 cmd = [ 'ffmpeg', '-i', input_file, '-c:v', 'libx264', '-preset', 'medium', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-y' # 覆盖输出文件 ] # 检查用户是否设置了分辨率参数 has_resolution_param = False for i, arg in enumerate(ffmpeg_args): if arg == '-vf' and i + 1 < len(ffmpeg_args): if 'scale=' in ffmpeg_args[i + 1]: has_resolution_param = True break # 如果用户没有设置分辨率,使用默认的1080p横屏设置 if not has_resolution_param: cmd.extend(['-vf', 'scale=1920:1080,setdar=16:9']) # 默认强制横屏1080p # 添加用户自定义参数 cmd.extend(ffmpeg_args) cmd.append(output_file) # 执行命令 self.log(f"正在转换: {os.path.basename(input_file)}") subprocess.run(cmd, check=True, capture_output=True, text=True) self.log(f"转换完成: {os.path.basename(output_file)}") return True except subprocess.CalledProcessError as e: self.log(f"转换失败: {os.path.basename(input_file)}") self.log(f"错误信息: {e.stderr}") return False except Exception as e: self.log(f"转换失败: {os.path.basename(input_file)}") self.log(f"错误信息: {str(e)}") return False def start_conversion(self): """开始转换过程""" # 验证输入 input_dir = self.input_dir.get().strip() if not input_dir: messagebox.showerror("错误", "请选择输入目录") return if not os.path.exists(input_dir): messagebox.showerror("错误", f"输入目录 '{input_dir}' 不存在") return output_dir = self.output_dir.get().strip() if not output_dir: messagebox.showerror("错误", "请选择输出目录") return try: threads = int(self.threads.get().strip()) if threads <= 0: raise ValueError except ValueError: messagebox.showerror("错误", "并发线程数必须是正整数") return # 解析FFmpeg参数 ffmpeg_params = self.ffmpeg_params.get().strip() ffmpeg_args = ffmpeg_params.split() if ffmpeg_params else [] # 更新UI状态 self.is_converting = True self.convert_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.status_var.set("转换中...") self.status_text.delete(1.0, tk.END) # 启动转换线程 def conversion_thread(): try: # 创建输出目录 os.makedirs(output_dir, exist_ok=True) # 获取所有视频文件 video_files = self.get_video_files(input_dir) if not video_files: self.log(f"错误: 输入目录 '{input_dir}' 中没有找到视频文件") return self.log(f"找到 {len(video_files)} 个视频文件") # 并发转换视频 success_count = 0 failure_count = 0 with ThreadPoolExecutor(max_workers=threads) as executor: future_to_video = {} for video_file in video_files: if not self.is_converting: break # 构建输出文件路径 relative_path = os.path.relpath(video_file, input_dir) output_file = os.path.join(output_dir, os.path.splitext(relative_path)[0] + '.mp4') # 创建输出文件的目录 os.makedirs(os.path.dirname(output_file), exist_ok=True) # 提交任务 future = executor.submit(self.convert_video, video_file, output_file, ffmpeg_args) future_to_video[future] = video_file # 处理结果 for future in as_completed(future_to_video): if not self.is_converting: # 取消所有未完成的任务 for f in future_to_video: if not f.done(): f.cancel() break video_file = future_to_video[future] try: result = future.result() if result: success_count += 1 else: failure_count += 1 except Exception as e: self.log(f"处理 {os.path.basename(video_file)} 时出错: {str(e)}") failure_count += 1 if self.is_converting: self.log(f"\n转换完成:") self.log(f"成功: {success_count}") self.log(f"失败: {failure_count}") messagebox.showinfo("转换完成", f"转换完成:\n成功: {success_count}\n失败: {failure_count}") else: self.log("转换已停止") messagebox.showinfo("转换停止", "转换已停止") except Exception as e: self.log(f"转换过程出错: {str(e)}") messagebox.showerror("错误", f"转换过程出错: {str(e)}") finally: # 恢复UI状态 self.is_converting = False self.convert_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.status_var.set("就绪") self.convert_thread = threading.Thread(target=conversion_thread) self.convert_thread.daemon = True self.convert_thread.start() def stop_conversion(self): """停止转换过程""" if messagebox.askyesno("确认", "确定要停止转换吗?"): self.is_converting = False self.status_var.set("停止中...") # 结束所有ffmpeg进程 try: import psutil for proc in psutil.process_iter(['pid', 'name']): if proc.info['name'] and 'ffmpeg' in proc.info['name'].lower(): proc.kill() self.log(f"已结束ffmpeg进程 (PID: {proc.info['pid']})") except ImportError: # 如果没有psutil,使用subprocess try: subprocess.run(['taskkill', '/F', '/IM', 'ffmpeg.exe'], capture_output=True) self.log("已结束所有ffmpeg进程") except: self.log("结束ffmpeg进程失败,请手动结束") except Exception as e: self.log(f"结束ffmpeg进程时出错: {str(e)}") def main(): """ 主函数 """ # 检查ffmpeg是否存在 if not os.path.exists('ffmpeg.exe'): # 尝试在系统路径中查找 try: subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True) except: messagebox.showerror("错误", "未找到ffmpeg.exe,请确保它与程序在同一目录下") return # 创建主窗口 root = tk.Tk() app = VideoConverterUI(root) # 运行主循环 root.mainloop() if __name__ == '__main__': main()