视频处理器 v1.2 是一款由是貔貅呀开发的视频编辑和处理工具,提供高效便捷的视频批量横转竖,主要功能:
导入与删除文件:轻松导入多个视频文件,删除不必要的文件。
暂停与继续处理:随时暂停和继续处理。
设置输出文件夹:选择视频处理后的存放文件夹。
FFmpeg 配置:自定义输出格式、视频编码器、音频编码器和线程数。
日志记录:实时记录处理日志。
使用步骤
导入文件
设置输出文件夹
配置FFmpeg
开始处理文件
查看处理进度
暂停与继续
特点
用户友好界面
多格式支持
高效多线程处理
实时日志显示
运行截图
文件代码如下
import os import ttkbootstrap as ttk from ttkbootstrap.constants import * from tkinter import filedialog, messagebox, END, Text, StringVar, IntVar from concurrent.futures import ThreadPoolExecutor, as_completed import subprocess import threading import psutil import re import sys def resource_path(relative_path): """ Get absolute path to resource, works for dev and for PyInstaller """ try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) class VideoProcessor: def __init__(self, master): self.master = master self.master.title("视频处理器") self.input_files = [] self.output_folder = "" self.process_thread = None self.pause_event = threading.Event() self.pause_event.set() # Start in the unpaused state self.ffmpeg_processes = [] # List to keep track of all ffmpeg processes self.is_closing = False self.output_format = StringVar(value="mp4") self.video_codec = StringVar(value="libx264") self.audio_codec = StringVar(value="aac") self.thread_count = IntVar(value=4) # Default to 4 threads self.create_widgets() self.master.protocol("WM_DELETE_WINDOW", self.on_closing) def create_widgets(self): frame = ttk.Frame(self.master, padding=10) frame.pack(fill=BOTH, expand=YES) self.file_list_frame = ttk.Frame(frame) self.file_list_frame.pack(fill=BOTH, expand=YES, pady=5) columns = ('序号', '文件夹名字', '进度') self.file_tree = ttk.Treeview(self.file_list_frame, columns=columns, show='headings') self.file_tree.heading('序号', text='序号') self.file_tree.heading('文件夹名字', text='文件夹名字') self.file_tree.heading('进度', text='进度') self.file_tree.column('序号', width=100, anchor='center') self.file_tree.column('文件夹名字', width=400, anchor='w') self.file_tree.column('进度', width=100, anchor='center') self.file_tree.pack(side=LEFT, fill=BOTH, expand=YES) scrollbar = ttk.Scrollbar(self.file_list_frame, orient="vertical", command=self.file_tree.yview) self.file_tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side=RIGHT, fill=Y) self.log_text = Text(frame, height=5, state='disabled') self.log_text.pack(fill=BOTH, expand=YES, pady=5) button_frame = ttk.Frame(frame) button_frame.pack(fill=BOTH, expand=YES) self.add_files_button = ttk.Button(button_frame, text="导入文件", command=self.add_files, bootstyle=PRIMARY) self.add_files_button.pack(side=LEFT, padx=5, pady=5) self.remove_files_button = ttk.Button(button_frame, text="删除文件", command=self.remove_files, bootstyle=DANGER) self.remove_files_button.pack(side=LEFT, padx=5, pady=5) self.pause_button = ttk.Button(button_frame, text="暂停/继续", command=self.toggle_pause, bootstyle=WARNING) self.pause_button.pack(side=LEFT, padx=5, pady=5) self.open_output_folder_button = ttk.Button(button_frame, text="打开文件", command=self.open_output_folder, bootstyle=SUCCESS) self.open_output_folder_button.pack(side=LEFT, padx=5, pady=5) self.set_output_folder_button = ttk.Button(button_frame, text="导出文件", command=self.set_output_folder, bootstyle=SUCCESS) self.set_output_folder_button.pack(side=LEFT, padx=5, pady=5) self.process_button = ttk.Button(button_frame, text="开始处理文件", command=self.start_processing, bootstyle=INFO) self.process_button.pack(side=RIGHT, padx=5, pady=5) config_frame = ttk.LabelFrame(frame, text="FFmpeg 配置") config_frame.pack(fill=BOTH, expand=YES, pady=5) ttk.Label(config_frame, text="输出格式:").pack(side=LEFT, padx=5, pady=5) ttk.OptionMenu(config_frame, self.output_format, "mp4", "mp4", "avi", "mov").pack(side=LEFT, padx=5, pady=5) ttk.Label(config_frame, text="视频编码器:").pack(side=LEFT, padx=5, pady=5) ttk.OptionMenu(config_frame, self.video_codec, "libx264", "libx264", "libx265", "mpeg4").pack(side=LEFT, padx=5, pady=5) ttk.Label(config_frame, text="音频编码器:").pack(side=LEFT, padx=5, pady=5) ttk.OptionMenu(config_frame, self.audio_codec, "aac", "aac", "mp3", "ac3").pack(side=LEFT, padx=5, pady=5) ttk.Label(config_frame, text="线程数:").pack(side=LEFT, padx=5, pady=5) ttk.Entry(config_frame, textvariable=self.thread_count).pack(side=LEFT, padx=5, pady=5) def add_files(self): files = filedialog.askopenfilenames(title="选择视频文件", filetypes=[("视频文件", "*.mp4 *.avi *.mov")]) for file in files: if file not in self.input_files: self.input_files.append(file) self.file_tree.insert('', END, values=(len(self.input_files), os.path.basename(file), "未处理")) self.log(f"导入文件: {file}") else: messagebox.showwarning("警告", f"文件已存在: {os.path.basename(file)}") def remove_files(self): selected_items = self.file_tree.selection() for item in selected_items: index = int(self.file_tree.item(item, 'values')[0]) - 1 del self.input_files[index] self.file_tree.delete(item) self.log("删除选中文件") self.refresh_file_list() def refresh_file_list(self): for item in self.file_tree.get_children(): self.file_tree.delete(item) for index, file in enumerate(self.input_files): self.file_tree.insert('', END, values=(index + 1, os.path.basename(file), "未处理")) def set_output_folder(self): self.output_folder = filedialog.askdirectory(title="选择输出文件夹") self.log(f"设置输出文件夹: {self.output_folder}") def start_processing(self): if not self.input_files or not self.output_folder: messagebox.showerror("错误", "请添加文件并设置输出文件夹。") return self.process_thread = threading.Thread(target=self.process_videos_concurrently) self.process_thread.start() def toggle_pause(self): if self.pause_event.is_set(): self.pause_event.clear() self.log("处理暂停") for process in self.ffmpeg_processes: proc = psutil.Process(process.pid) proc.suspend() else: self.pause_event.set() self.log("处理继续") for process in self.ffmpeg_processes: proc = psutil.Process(process.pid) proc.resume() def open_output_folder(self): if self.output_folder: os.startfile(self.output_folder) self.log(f"打开输出文件夹: {self.output_folder}") else: messagebox.showerror("错误", "请先设置输出文件夹。") def log(self, message): if not self.is_closing: self.master.after(0, self._log, message) def _log(self, message): if not self.is_closing: self.log_text.configure(state='normal') self.log_text.insert(END, message + '\n') self.log_text.configure(state='disabled') self.log_text.yview(END) def update_tree_status(self, index, status): if not self.is_closing: self.master.after(0, self._update_tree_status, index, status) def _update_tree_status(self, index, status): if not self.is_closing: self.file_tree.item(self.file_tree.get_children()[index], values=(index + 1, os.path.basename(self.input_files[index]), status)) def process_videos_concurrently(self): max_workers = self.thread_count.get() with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(self.process_video, index, input_file) for index, input_file in enumerate(self.input_files)] for future in as_completed(futures): future.result() def process_video(self, index, input_file): ffmpeg_path = resource_path(os.path.join("ffmpeg_folder", "ffmpeg")) filename = os.path.basename(input_file) output_file = os.path.join(self.output_folder, f"processed_{filename}.{self.output_format.get()}") if os.path.exists(output_file): overwrite = messagebox.askyesno("文件已存在", f"{output_file} 已存在,是否覆盖?") if not overwrite: self.update_tree_status(index, "跳过") return cmd = [ ffmpeg_path, "-y", # 自动覆盖输出文件 "-i", input_file, "-vf", "scale='if(gt(iw/ih,9/16),1080,-2)':'if(gt(iw/ih,9/16),-2,1920)',pad=1080:1920:(1080-iw)/2:(1920-ih)/2", "-c:v", self.video_codec.get(), "-c:a", self.audio_codec.get(), output_file ] self.log(f"开始处理: {filename}") self.update_tree_status(index, "处理中") try: startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True, encoding='utf-8', startupinfo=startupinfo) self.ffmpeg_processes.append(process) for line in process.stderr: if self.is_closing: break progress = self.parse_progress(line) if progress: self.update_tree_status(index, progress) process.wait() except Exception as e: self.log(f"处理文件时出错: {filename} - {str(e)}") self.update_tree_status(index, "处理失败") process = None self.current_index = None return if self.is_closing: self.update_tree_status(index, "未完成") else: self.log(f"完成处理: {filename}") self.update_tree_status(index, "已完成") self.ffmpeg_processes.remove(process) def parse_progress(self, line): match = re.search(r'time=(\d+:\d+:\d+\.\d+)', line) if match: return f"进度: {match.group(1)}" return None def on_closing(self): self.is_closing = True for process in self.ffmpeg_processes: proc = psutil.Process(process.pid) proc.terminate() self.master.destroy() if __name__ == "__main__": root = ttk.Window(themename="superhero") app = VideoProcessor(master=root) root.mainloop()