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()