让AI基于python-tkinter写一个视频转码小工具
·
起因是整理素材时发现文件属性中多了一个“转换压缩”菜单;
煮波还以为是微软的功能,点进去发现居然是国内厂商的应用,结果要会员,伪装的还挺像;

以前手敲代码的日子一去不复返,让AI使用python-tkinter生成一个调用ffmpeg的视频转码小工具,直接秒了;
目前支持单文件或批量转码;支持视频编码、码率(比特率)、帧率、分辨率、音频编码等
使用方法:
方式一:通过pyinstaller打包了一个FFmpegCudaConverter.exe,应用内已包含FFmpeg,双击即用,杀毒软件可能误报,信任即可,下载入口在文章顶部;
方式二:源码附上,安装过python环境可cmd运行.py文件,或用IDEA点开即玩,需要另外准备FFmpqg.exe程序,请自行从官网下载,小工具会根据它的默认安装路径或者系统path环境变量路径找到FFmpeg.exe,无特殊存放路径要求;

import os
import subprocess
import threading
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext, messagebox
from tkinter.ttk import Label, Button, Entry, Spinbox, Combobox, Progressbar
class FFmpegCudaConverter(tk.Tk):
def __init__(self):
super().__init__()
self.title("FFmpeg CUDA 视频转码工具")
self.geometry("850x750")
self.minsize(650, 600)
# 全局变量
self.ffmpeg_path = self.find_ffmpeg()
self.is_converting = False
self.convert_thread = None
self.total_duration = 0 # 输入文件总时长(用于计算进度)
# 初始化UI
self.init_ui()
def init_ui(self):
# 主容器(网格布局)
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
# 配置滚动区域
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
# 在canvas中创建窗口
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 布局
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
canvas.bind_all("<MouseWheel>", _on_mousewheel)
canvas.bind("<MouseWheel>", _on_mousewheel)
# 主容器(网格布局)- 使用scrollable_frame作为父容器
main_frame = ttk.Frame(self.scrollable_frame, padding="20")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重(让组件自适应窗口大小)
self.scrollable_frame.columnconfigure(0, weight=1)
self.scrollable_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.columnconfigure(2, weight=1)
main_frame.columnconfigure(3, weight=1)
main_frame.rowconfigure(12, weight=1) # 日志区域占满剩余空间
# 1. 标题
title_label = Label(main_frame, text="FFmpeg CUDA 硬件加速转码", font=("Arial", 16, "bold"))
title_label.grid(row=0, column=0, columnspan=4, pady=(0, 20), sticky=tk.W)
# 2. 文件选择区域
# 在文件选择区域下方添加批量文件选择
# 在"浏览"按钮旁边添加批量选择按钮
Button(main_frame, text="批量选择", command=self.select_multiple_files).grid(row=1, column=4, pady=5,
padx=(0, 5))
# 添加批量文件列表显示区域
self.file_list_frame = ttk.LabelFrame(main_frame, text="待转码文件列表", padding="10")
self.file_list_frame.grid(row=10, column=0, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
self.file_list_frame.columnconfigure(0, weight=1)
self.file_list_frame.rowconfigure(0, weight=1)
# 创建列表框和滚动条
self.file_listbox = tk.Listbox(self.file_list_frame, selectmode=tk.EXTENDED)
self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
listbox_scrollbar = ttk.Scrollbar(self.file_list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
listbox_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.file_listbox.configure(yscrollcommand=listbox_scrollbar.set)
# 添加列表操作按钮
list_btn_frame = ttk.Frame(self.file_list_frame)
list_btn_frame.grid(row=1, column=0, columnspan=2, pady=5)
Button(list_btn_frame, text="移除选中", command=self.remove_selected_files).grid(row=0, column=0, padx=5)
Button(list_btn_frame, text="清空列表", command=self.clear_file_list).grid(row=0, column=1, padx=5)
# 输入文件
Label(main_frame, text="输入文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.input_var = tk.StringVar()
input_entry = Entry(main_frame, textvariable=self.input_var, width=40)
input_entry.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5, padx=5)
Button(main_frame, text="浏览", command=self.select_input_file).grid(row=1, column=3, pady=5)
# 输出目录
Label(main_frame, text="输出目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_var = tk.StringVar()
output_entry = Entry(main_frame, textvariable=self.output_var, width=40)
output_entry.grid(row=2, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5, padx=5)
Button(main_frame, text="浏览", command=self.select_output_dir).grid(row=2, column=3, pady=5)
# 输出文件名
Label(main_frame, text="输出文件名:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.filename_var = tk.StringVar()
filename_entry = Entry(main_frame, textvariable=self.filename_var, width=40)
filename_entry.grid(row=3, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=5, padx=5)
filename_entry.insert(0, "")
Label(main_frame, text="(不带后缀,默认与输入文件相同)").grid(row=4, column=1, columnspan=3, sticky=tk.W, padx=5)
# 3. 视频配置区域(分组框)
video_frame = ttk.LabelFrame(main_frame, text="视频配置", padding="10")
video_frame.grid(row=5, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)
video_frame.columnconfigure(1, weight=1)
video_frame.columnconfigure(3, weight=1)
# 视频编码器
Label(video_frame, text="视频编码器:").grid(row=0, column=0, sticky=tk.W, pady=5, padx=5)
self.video_codec_var = tk.StringVar(value="hevc_nvenc")
video_codec_combo = Combobox(video_frame, textvariable=self.video_codec_var, state="readonly", width=15)
video_codec_combo["values"] = ("hevc_nvenc", "h264_nvenc")
video_codec_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
# 添加提示信息标签
info_label = Label(video_frame, text="22222", foreground="blue", cursor="hand2", font=("Arial", 10))
info_label.grid(row=0, column=2, padx=(5, 0), sticky=tk.W)
info_label.bind("<Enter>", self.show_codec_tooltip)
info_label.bind("<Leave>", self.hide_tooltip)
# 配置列权重以确保正确布局
video_frame.columnconfigure(1, weight=1)
video_frame.columnconfigure(2, weight=0) # 提示标签不需要扩展
# 创建一个工具提示标签(初始隐藏)
self.tooltip = tk.Label(self, text="", background="lightyellow", relief="solid", borderwidth=1)
self.tooltip.place_forget()
# 编码预设
Label(video_frame, text="编码预设:").grid(row=0, column=2, sticky=tk.W, pady=5, padx=5)
self.preset_var = tk.StringVar(value="fast")
preset_combo = Combobox(video_frame, textvariable=self.preset_var, state="readonly")
preset_combo["values"] = ( "fast", "medium", "slow")
preset_combo.grid(row=0, column=3, sticky=(tk.W, tk.E), pady=5, padx=5)
# 视频比特率
Label(video_frame, text="视频比特率 (kbps):").grid(row=1, column=0, sticky=tk.W, pady=5, padx=5)
self.bitrate_var = tk.StringVar(value="300")
bitrate_spin = Spinbox(video_frame, from_=100, to=10000, textvariable=self.bitrate_var, increment=100, width=10)
bitrate_spin.grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)
# 帧率
Label(video_frame, text="帧率 (fps):").grid(row=1, column=2, sticky=tk.W, pady=5, padx=5)
self.fps_var = tk.StringVar(value="15")
fps_spin = Spinbox(video_frame, from_=1, to=60, textvariable=self.fps_var, increment=1, width=10)
fps_spin.grid(row=1, column=3, sticky=tk.W, pady=5, padx=5)
# 分辨率
Label(video_frame, text="分辨率:").grid(row=2, column=0, sticky=tk.W, pady=5, padx=5)
self.resolution_var = tk.StringVar(value="640x360")
resolution_combo = Combobox(video_frame, textvariable=self.resolution_var, state="readonly")
resolution_combo["values"] = ("640x360", "854x480", "1280x720", "1920x1080", "自定义")
resolution_combo.bind("<<ComboboxSelected>>", self.toggle_custom_resolution)
resolution_combo.grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
# 自定义分辨率
self.custom_res_frame = ttk.Frame(video_frame)
Label(self.custom_res_frame, text="宽:").grid(row=0, column=0, sticky=tk.W)
self.width_var = tk.StringVar(value="640")
width_spin = Spinbox(self.custom_res_frame, from_=160, to=4096, textvariable=self.width_var, width=8)
width_spin.grid(row=0, column=1, padx=5)
Label(self.custom_res_frame, text="x").grid(row=0, column=2)
Label(self.custom_res_frame, text="高:").grid(row=0, column=3, padx=5)
self.height_var = tk.StringVar(value="360")
height_spin = Spinbox(self.custom_res_frame, from_=90, to=2160, textvariable=self.height_var, width=8)
height_spin.grid(row=0, column=4, padx=5)
self.custom_res_frame.grid(row=2, column=2, columnspan=2, sticky=tk.W, pady=5, padx=5)
#self.custom_res_frame.config(state=tk.DISABLED) # 默认禁用
# 4. 音频配置区域(分组框)
audio_frame = ttk.LabelFrame(main_frame, text="音频配置", padding="10")
audio_frame.grid(row=6, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)
audio_frame.columnconfigure(1, weight=1)
audio_frame.columnconfigure(3, weight=1)
# 音频编码器
Label(audio_frame, text="音频编码器:").grid(row=0, column=0, sticky=tk.W, pady=5, padx=5)
self.audio_codec_var = tk.StringVar(value="aac")
audio_codec_combo = Combobox(audio_frame, textvariable=self.audio_codec_var, state="readonly")
audio_codec_combo["values"] = ("aac", "mp3", "opus")
audio_codec_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
# 音频比特率
Label(audio_frame, text="音频比特率 (kbps):").grid(row=0, column=2, sticky=tk.W, pady=5, padx=5)
self.audio_bitrate_var = tk.StringVar(value="64")
audio_bitrate_spin = Spinbox(audio_frame, from_=32, to=384, textvariable=self.audio_bitrate_var, increment=32, width=10)
audio_bitrate_spin.grid(row=0, column=3, sticky=tk.W, pady=5, padx=5)
# 5. 控制按钮区域
btn_frame = ttk.Frame(main_frame)
btn_frame.grid(row=7, column=0, columnspan=4, pady=10)
self.start_btn = Button(btn_frame, text="开始转码", command=self.start_convert)
self.start_btn.grid(row=0, column=0, padx=10)
self.stop_btn = Button(btn_frame, text="停止转码", command=self.stop_convert, state=tk.DISABLED)
self.stop_btn.grid(row=0, column=1, padx=10)
# 6. 进度条
self.progress_var = tk.DoubleVar(value=0)
progress_bar = Progressbar(main_frame, variable=self.progress_var, maximum=100)
progress_bar.grid(row=8, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=5)
# 7. 日志输出区域
log_frame = ttk.LabelFrame(main_frame, text="转码日志", padding="10")
log_frame.grid(row=9, column=0, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.log_text = scrolledtext.ScrolledText(log_frame, width=80, height=15, state=tk.DISABLED)
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 初始化日志
self.append_log("=== FFmpeg CUDA 转码工具 ===")
self.append_log(f"FFmpeg路径:{self.ffmpeg_path if os.path.exists(self.ffmpeg_path) else '未找到,请检查安装'}")
def show_codec_tooltip(self, event):
"""显示编码器提示信息"""
# 调整坐标计算方式,确保在窗口可视范围内
x = event.widget.winfo_x() + event.widget.winfo_width() + 10 # 控件右侧10像素
y = event.widget.winfo_y() # 与控件同一水平位置
# 将坐标转换为相对于主窗口的位置
x += self.scrollable_frame.winfo_x() + self.winfo_x()
y += self.scrollable_frame.winfo_y() + self.winfo_y()
# 创建工具提示
tooltip_text = ("hevc_nvenc: H.265/HEVC编码器,压缩率更高但兼容性稍差\n"
"h264_nvenc: H.264编码器,兼容性好但文件相对较大")
# 确保工具提示使用主窗口作为父容器,并设置适当样式
self.tooltip = tk.Label(
self, # 使用主窗口作为父容器,确保能在所有控件上方显示
text=tooltip_text,
background="lightyellow",
relief="solid",
borderwidth=1,
wraplength=300, # 自动换行
padx=5,
pady=5
)
self.tooltip.place(x=x, y=y)
def hide_tooltip(self, event):
"""隐藏工具提示"""
if hasattr(self, 'tooltip'):
self.tooltip.destroy()
def select_multiple_files(self):
"""选择多个输入视频文件"""
file_paths = filedialog.askopenfilenames(
title="选择多个视频文件",
filetypes=[("视频文件", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv *.webm"), ("所有文件", "*.*")]
)
if file_paths:
for file_path in file_paths:
# 避免重复添加
if file_path not in self.get_file_list_items():
self.file_listbox.insert(tk.END, file_path)
# 如果还没有设置输出目录,则自动设置为第一个文件的目录
if not self.output_var.get() and file_paths:
self.output_var.set(os.path.dirname(file_paths[0]))
def get_file_list_items(self):
"""获取文件列表中的所有项目"""
items = []
for i in range(self.file_listbox.size()):
items.append(self.file_listbox.get(i))
return items
def remove_selected_files(self):
"""移除选中的文件"""
selected_indices = list(self.file_listbox.curselection())
# 从后往前删除,避免索引变化问题
for index in reversed(selected_indices):
self.file_listbox.delete(index)
def clear_file_list(self):
"""清空文件列表"""
self.file_listbox.delete(0, tk.END)
def find_ffmpeg(self):
"""查找系统中的ffmpeg可执行文件"""
if os.name == 'nt': # Windows
# 常见安装路径
common_paths = [
os.path.join(os.environ.get('ProgramFiles', ''), 'ffmpeg', 'bin', 'ffmpeg.exe'),
os.path.join(os.environ.get('ProgramFiles(x86)', ''), 'ffmpeg', 'bin', 'ffmpeg.exe'),
os.path.join(os.environ.get('USERPROFILE', ''), 'AppData', 'Local', 'ffmpeg', 'bin', 'ffmpeg.exe'),
]
for path in common_paths:
if os.path.exists(path):
return path
# 检查系统PATH
for path in os.environ['PATH'].split(os.pathsep):
ffmpeg_path = os.path.join(path, 'ffmpeg.exe')
if os.path.exists(ffmpeg_path):
return ffmpeg_path
else: # Linux/Mac
for path in os.environ['PATH'].split(os.pathsep):
ffmpeg_path = os.path.join(path, 'ffmpeg')
if os.path.exists(ffmpeg_path):
return ffmpeg_path
return "ffmpeg" # 兜底(希望系统能找到)
def select_input_file(self):
"""选择输入视频文件"""
file_path = filedialog.askopenfilename(
title="选择视频文件",
filetypes=[("视频文件", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv *.webm"), ("所有文件", "*.*")]
)
if file_path:
self.input_var.set(file_path)
# 自动填充输出文件名(如果未手动输入)
if not self.filename_var.get():
self.filename_var.set(os.path.splitext(os.path.basename(file_path))[0])
def select_output_dir(self):
"""选择输出目录"""
dir_path = filedialog.askdirectory(title="选择输出目录")
if dir_path:
self.output_var.set(dir_path)
def toggle_custom_resolution(self, event):
"""切换自定义分辨率的启用状态"""
if self.resolution_var.get() == "自定义":
# 启用自定义分辨率控件
for widget in self.custom_res_frame.winfo_children():
widget.config(state=tk.NORMAL)
else:
# 禁用自定义分辨率控件
for widget in self.custom_res_frame.winfo_children():
widget.config(state=tk.DISABLED)
def get_resolution(self):
"""获取分辨率参数"""
if self.resolution_var.get() == "自定义":
return f"{self.width_var.get()}x{self.height_var.get()}"
else:
return self.resolution_var.get()
def build_ffmpeg_command(self):
"""构建FFmpeg命令"""
input_file = self.input_var.get().strip()
output_dir = self.output_var.get().strip()
output_name = self.filename_var.get().strip()
# 验证参数
if not input_file or not os.path.exists(input_file):
raise ValueError("请选择有效的输入文件!")
if not output_dir or not os.path.isdir(output_dir):
raise ValueError("请选择有效的输出目录!")
if not output_name:
output_name = os.path.splitext(os.path.basename(input_file))[0]
# 输出文件路径(默认MP4容器,适配HEVC/H264)
output_file = os.path.join(output_dir, f"{output_name}.mp4")
# 构建命令列表
cmd = [
self.ffmpeg_path,
"-hwaccel", "cuda", # CUDA硬件加速
"-i", input_file,
"-c:v", self.video_codec_var.get(),
"-preset", self.preset_var.get(),
"-b:v", f"{self.bitrate_var.get()}k",
"-r", self.fps_var.get(),
"-s", self.get_resolution(),
"-c:a", self.audio_codec_var.get(),
"-b:a", f"{self.audio_bitrate_var.get()}k",
"-y", # 覆盖输出文件
output_file
]
return cmd, output_file
# 在 FFmpegCudaConverter 类中添加以下方法:
def convert_file_list(self, file_list):
"""批量转码文件列表"""
if not file_list:
messagebox.showwarning("警告", "没有可转码的文件!")
return
output_dir = self.output_var.get().strip()
if not output_dir or not os.path.isdir(output_dir):
messagebox.showerror("错误", "请选择有效的输出目录!")
return
# 更新按钮状态
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.is_converting = True
# 启动批量转码线程
self.convert_thread = threading.Thread(target=self.run_batch_ffmpeg, args=(file_list,))
self.convert_thread.daemon = True
self.convert_thread.start()
def start_convert(self):
"""开始转码(主线程)"""
if self.is_converting:
messagebox.showwarning("警告", "正在转码中,请勿重复点击!")
return
# 检查是否有批量文件
file_list = self.get_file_list_items()
single_file = self.input_var.get().strip()
# 确定转码模式
if file_list:
# 批量转码模式
self.convert_file_list(file_list)
elif single_file and os.path.exists(single_file):
# 单文件转码模式
try:
self.cmd, self.output_file = self.build_ffmpeg_command()
self.append_log(f"\n转码命令:{' '.join(self.cmd)}")
self.append_log(f"输出文件:{self.output_file}")
self.append_log("开始转码...")
# 重置进度条
self.progress_var.set(0)
# 更新按钮状态
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.is_converting = True
# 启动子线程执行转码(避免阻塞UI)
self.convert_thread = threading.Thread(target=self.run_ffmpeg)
self.convert_thread.daemon = True
self.convert_thread.start()
except ValueError as e:
messagebox.showerror("错误", str(e))
self.append_log(f"错误:{str(e)}")
except Exception as e:
messagebox.showerror("错误", f"未知错误:{str(e)}")
self.append_log(f"未知错误:{str(e)}")
else:
messagebox.showerror("错误", "请选择要转码的文件!")
def run_batch_ffmpeg(self, file_list):
"""执行批量FFmpeg命令(子线程)"""
total_files = len(file_list)
completed_files = 0
try:
for i, input_file in enumerate(file_list):
if not self.is_converting: # 用户停止了转码
break
self.append_log(f"\n[{i + 1}/{total_files}] 正在处理: {os.path.basename(input_file)}")
# 构建单个文件的输出路径
output_dir = self.output_var.get().strip()
filename = os.path.splitext(os.path.basename(input_file))[0]
output_file = os.path.join(output_dir, f"{filename}.mp4")
# 构建命令
cmd = [
self.ffmpeg_path,
"-hwaccel", "cuda", # CUDA硬件加速
"-i", input_file,
"-c:v", self.video_codec_var.get(),
"-preset", self.preset_var.get(),
"-b:v", f"{self.bitrate_var.get()}k",
"-r", self.fps_var.get(),
"-s", self.get_resolution(),
"-c:a", self.audio_codec_var.get(),
"-b:a", f"{self.audio_bitrate_var.get()}k",
"-y", # 覆盖输出文件
output_file
]
self.append_log(f"转码命令:{' '.join(cmd)}")
self.append_log(f"输出文件:{output_file}")
# 执行转码
process = None
try:
# 启动FFmpeg进程
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
errors='ignore'
)
# 读取输出日志
while self.is_converting and process.poll() is None:
line = process.stdout.readline()
if line:
self.append_log(line.strip())
# 检查转码结果
if process.poll() == 0 and self.is_converting:
completed_files += 1
self.append_log(f"✅ [{i + 1}/{total_files}] 转码完成!")
else:
self.append_log(f"❌ [{i + 1}/{total_files}] 转码失败或被中断!")
except Exception as e:
self.append_log(f"转码错误:{str(e)}")
finally:
if process and process.poll() is None:
process.terminate()
process.wait()
# 批量转码完成
if self.is_converting:
self.append_log(f"\n=== 批量转码完成 ===")
self.append_log(f"成功转码 {completed_files}/{total_files} 个文件")
messagebox.showinfo("成功", f"批量转码完成!\n成功转码 {completed_files}/{total_files} 个文件")
except Exception as e:
self.append_log(f"批量转码错误:{str(e)}")
finally:
# 恢复UI状态
self.is_converting = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def run_ffmpeg(self):
"""执行FFmpeg命令(子线程)"""
process = None
try:
# 启动FFmpeg进程
process = subprocess.Popen(
self.cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
errors='ignore'
)
# 读取输出日志并更新进度
while self.is_converting and process.poll() is None:
line = process.stdout.readline()
if line:
self.append_log(line.strip())
self.parse_progress(line) # 解析进度
# 转码结束处理
if process.poll() == 0 and self.is_converting:
self.append_log("✅ 转码完成!")
self.progress_var.set(100)
else:
self.append_log("❌ 转码被中断或失败!")
except Exception as e:
self.append_log(f"转码错误:{str(e)}")
finally:
# 清理进程
if process and process.poll() is None:
process.terminate()
process.wait()
# 恢复UI状态
self.is_converting = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
if self.is_converting is False and process.poll() == 0:
messagebox.showinfo("成功", f"转码完成!\n输出文件:{self.output_file}")
def parse_progress(self, line):
"""解析FFmpeg输出的进度信息"""
# 匹配格式:frame= 1234 fps= 30 q=28.0 size= 12345kB time=00:00:49.36 bitrate= 2048.0kbits/s speed=1.2x
if "time=" in line and "bitrate=" in line:
try:
# 首次解析时获取输入文件总时长
if self.total_duration == 0:
self.get_input_duration()
# 提取当前时间(HH:MM:SS.ms)
time_str = None
for part in line.split():
if part.startswith("time="):
time_str = part[5:]
break
if time_str and self.total_duration > 0:
# 解析当前时间为秒数
hh, mm, ss_ms = time_str.split(":")
ss, ms = ss_ms.split(".")
current_seconds = int(hh)*3600 + int(mm)*60 + int(ss) + int(ms)/100
# 计算进度百分比
progress = (current_seconds / self.total_duration) * 100
self.progress_var.set(min(progress, 100))
except:
pass # 解析失败不影响转码
def get_input_duration(self):
"""获取输入文件总时长(用于进度计算)"""
try:
cmd = [
self.ffmpeg_path,
"-i", self.input_var.get().strip(),
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1"
]
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='ignore',
timeout=10
)
if result.returncode == 0:
self.total_duration = float(result.stdout.strip())
except:
self.total_duration = 0 # 获取失败则不显示进度
def stop_convert(self):
"""停止转码"""
if messagebox.askyesno("确认", "是否要停止转码?"):
self.is_converting = False
self.append_log("正在停止转码...")
self.stop_btn.config(state=tk.DISABLED)
def append_log(self, text):
"""添加日志到文本框(线程安全)"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, text + "\n")
self.log_text.see(tk.END) # 自动滚动到底部
self.log_text.config(state=tk.DISABLED)
if __name__ == "__main__":
app = FFmpegCudaConverter()
app.mainloop()
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)