简介
视频处理器 v1.3 是一款由是貔貅呀开发的视频编辑和处理工具,提供高效便捷的视频批量横转竖,主要功能:
- 导入与删除文件:轻松导入多个视频文件,删除不必要的文件。
- 暂停与继续处理:随时暂停和继续处理。
- 设置输出文件夹:选择视频处理后的存放文件夹。
- FFmpeg 配置:自定义输出格式、视频编码器、音频编码器和线程数。
- 日志记录:实时记录处理日志。
使用步骤
- 导入文件
- 设置输出文件夹
- 配置FFmpeg
- 开始处理文件
- 查看处理进度
- 暂停与继续
特点
- 用户友好界面
- 多格式支持
- 高效多线程处理
- 实时日志显示
- 运行截图
效果展示
修复功能:
- 修复全选删除后程序出问题
- 修复了初始4条线程改为初始1条
增加功能:
- 新增直接拖拽视频到列表功能
- 新增高斯模糊功能
- 新增菜单栏(单纯忘了)
- 新增作者名(单纯忘了)
待上线新功能
- FFmpeg配置将进行修改,感觉挺鸡肋
- 批量竖转横(只能增加黑边或者高斯模糊,如要完整视频则只能拉长视频)
- 批量修改文件名(看需求量多不多)
- 图片批量修改大小,无ai辅助只是单纯放大或者缩小因为本人接触不深,(看需求量多不多)
源码
import os import ttkbootstrap as ttk from ttkbootstrap.constants import * from tkinter import filedialog, messagebox, END, Text, StringVar, IntVar, BooleanVar, Menu from concurrent.futures import ThreadPoolExecutor, as_completed import subprocess import threading import psutil import re import sys from tkinterdnd2 import TkinterDnD, DND_FILES 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("视频处理器 吾爱作者:是谁的大海(是貔貅呀) 版本:1.3") 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=1) # Default to 1 threads self.apply_blur = BooleanVar(value=False) # Boolean to check if blur should be applied self.create_widgets() self.create_menu() 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) self.blur_checkbox = ttk.Checkbutton(config_frame, text="应用高斯模糊效果", variable=self.apply_blur) self.blur_checkbox.pack(side=LEFT, padx=5, pady=5) # Set up drag and drop self.master.drop_target_register(DND_FILES) self.master.dnd_bind('<<Drop>>', self.drop_files) def create_menu(self): menu_bar = Menu(self.master) self.master.config(menu=menu_bar) help_menu = Menu(menu_bar, tearoff=0) menu_bar.add_cascade(label="帮助", menu=help_menu) help_menu.add_command(label="使用说明", command=self.show_usage_instructions) help_menu.add_command(label="软件具体说明", command=self.show_software_details) def show_usage_instructions(self): instructions = ( "使用说明:\n" "1. 导入文件:点击“导入文件”按钮,选择需要处理的视频文件,或将视频文件拖拽到软件窗口中。\n" "2. 设置输出文件夹:点击“导出文件”按钮,选择一个文件夹作为输出文件夹。\n" "3. 配置FFmpeg参数:在“FFmpeg 配置”区域,选择输出格式、视频编码器、音频编码器、线程数,并可选择是否应用高斯模糊效果。\n" "4. 开始处理:点击“开始处理文件”按钮,开始批量处理视频文件。处理过程中可以查看处理进度和日志信息。\n" "5. 查看输出文件:点击“打开文件”按钮,打开输出文件夹查看处理完成的视频文件。\n" "6. 删除文件:选择文件列表中的文件,点击“删除文件”按钮删除不需要处理的文件。\n" ) messagebox.showinfo("使用说明", instructions) def show_software_details(self): details = ( "仅供学习,切勿使用到其他用途\n" "1. 输出格式:支持MP4、AVI和MOV等常见格式,用户可自定义选择。\n" "2. 视频压缩:默认使用libx264视频编码器和aac音频编码器,支持高效视频压缩,用户可自定义选择其他编码器。\n" "3. 视频裁剪:适用于将1920x1080横屏视频裁剪成9:16竖屏视频,不会变形。\n" "4. 高斯模糊:可选应用高斯模糊效果,适用于特殊视频效果需求。\n" "5. 多线程处理:支持多线程并发处理,用户可自定义线程数,提高处理效率。\n" ) messagebox.showinfo("软件具体说明", details) def drop_files(self, event): files = self.master.tk.splitlist(event.data) for file in files: if file not in self.input_files and file.lower().endswith(('.mp4', '.avi', '.mov')): 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 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() indices_to_remove = [] for item in selected_items: values = self.file_tree.item(item, 'values') if values: index = int(values[0]) - 1 indices_to_remove.append(index) self.file_tree.delete(item) # 删除索引列表中的元素(倒序删除避免索引问题) for index in sorted(indices_to_remove, reverse=True): del self.input_files[index] 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 if self.apply_blur.get(): cmd = [ ffmpeg_path, "-y", # 自动覆盖输出文件 "-i", input_file, "-vf", "split[a][b];[a]scale=1080:1920,boxblur=10:5[1];[b]scale=1080:ih*1080/iw[2];[1][2]overlay=0:(H-h)/2", "-c:v", self.video_codec.get(), "-crf", "18", "-preset", "veryfast", "-aspect", "9:16", "-c:a", self.audio_codec.get(), output_file ] else: 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(), "-crf", "18", "-preset", "veryfast", "-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, "处理失败") 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 = TkinterDnD.Tk() root.title("视频处理器") root.geometry("870x520") # Set the window size to 870x520 root.resizable(False, False) # Make the window non-resizable app = VideoProcessor(master=root) root.mainloop()
1 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
2 本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
3 本站资源大多存储在云盘,如发现链接失效,请联系我们第一时间更新。联系青禾站长