起因是整理素材时发现文件属性中多了一个“转换压缩”菜单;

煮波还以为是微软的功能,点进去发现居然是国内厂商的应用,结果要会员,伪装的还挺像;

         以前手敲代码的日子一去不复返,让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()

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐