python --视频转码工具h265转h264
·
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import subprocess
import threading
import time
import sys
base_dir = os.path.dirname(__file__)
class H265ToH264Converter(object):
def __init__(self, root):
self.ffmpeg_path = os.path.join(base_dir, 'ffmpeg/bin/ffmpeg.exe')
self.ffprobe_path = os.path.join(base_dir, 'ffmpeg/bin/ffprobe.exe')
self.root = root
self.root.title("视频转码工具")
self.root.geometry("700x450")
icon_img = tk.PhotoImage(file=os.path.join(base_dir, 'logo.png'))
self.root.iconphoto(True, icon_img) # True表示应用到所有子窗口
self.root.resizable(False, False)
# 全局变量
self.source_dir = tk.StringVar() # 源文件夹
self.output_dir = tk.StringVar() # 输出文件夹
self.is_converting = False # 是否正在转码
self.convert_thread = None # 转码线程
self.total_files = 0 # 总待处理MP4数
self.processed_files = 0 # 已处理数
self.skipped_files = 0 # 跳过的H264文件数
self.failed_files = 0 # 失败文件数
# 创建UI界面
self._create_widgets()
def _create_widgets(self):
"""创建界面元素(移除Emoji,替换为ASCII/中文符号)"""
# 1. 源文件夹选择区域
frame_source = ttk.LabelFrame(self.root, text="【源文件夹】存放MP4视频")
frame_source.pack(fill="x", padx=20, pady=10)
ttk.Entry(frame_source, textvariable=self.source_dir, state="readonly", width=60).pack(side="left", padx=10, pady=5)
ttk.Button(frame_source, text="选择文件夹", command=self._select_source_dir).pack(side="left", padx=5, pady=5)
# 2. 输出文件夹选择区域
frame_output = ttk.LabelFrame(self.root, text="【输出文件夹】保存转码后的视频")
frame_output.pack(fill="x", padx=20, pady=10)
ttk.Entry(frame_output, textvariable=self.output_dir, state="readonly", width=60).pack(side="left", padx=10, pady=5)
ttk.Button(frame_output, text="选择文件夹", command=self._select_output_dir).pack(side="left", padx=5, pady=5)
# 3. 控制按钮区域
frame_control = ttk.Frame(self.root)
frame_control.pack(fill="x", padx=20, pady=10)
self.btn_start = ttk.Button(frame_control, text="开始转码", command=self._start_convert)
self.btn_start.pack(side="left", padx=10, pady=5)
self.btn_stop = ttk.Button(frame_control, text="停止转码", command=self._stop_convert, state="disabled")
self.btn_stop.pack(side="left", padx=10, pady=5)
# 4. 进度显示区域
frame_progress = ttk.LabelFrame(self.root, text="【转码进度】")
frame_progress.pack(fill="both", padx=20, pady=10, expand=True)
# 进度条
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(frame_progress, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill="x", padx=10, pady=5)
# 进度文本
self.label_progress = ttk.Label(frame_progress, text="等待开始 | 总文件:0 | 已处理:0 | 跳过:0 | 失败:0")
self.label_progress.pack(padx=10, pady=5)
# 日志显示框
self.text_log = tk.Text(frame_progress, height=10, width=80)
self.text_log.pack(padx=10, pady=5)
self.text_log.config(state="disabled") # 初始只读
def _select_source_dir(self):
"""选择源文件夹"""
dir_path = filedialog.askdirectory(title="选择存放MP4的源文件夹")
if dir_path:
self.source_dir.set(dir_path)
self._log(f"已选择源文件夹:{dir_path}")
def _select_output_dir(self):
"""选择输出文件夹"""
dir_path = filedialog.askdirectory(title="选择转码后视频的输出文件夹")
if dir_path:
self.output_dir.set(dir_path)
self._log(f"已选择输出文件夹:{dir_path}")
def _log(self, msg):
"""日志输出(带时间戳,移除Emoji)"""
self.text_log.config(state="normal")
timestamp = time.strftime("%H:%M:%S")
# 替换日志中的Emoji为中文符号
msg = msg.replace("✅", "【成功】").replace("❌", "【错误】").replace("⚠️", "【警告】")
msg = msg.replace("🔄", "【转码】").replace("⏭️", "【跳过】").replace("🚀", "【启动】").replace("🛑", "【停止】")
msg = msg.replace("📌", "【提示】").replace("📊", "【统计】")
self.text_log.insert(tk.END, f"[{timestamp}] {msg}\n")
self.text_log.see(tk.END) # 自动滚动到底部
self.text_log.config(state="disabled")
def _get_video_codec(self, video_path):
"""
检测视频编码格式(通过ffprobe命令)
返回:h264/h265/unknown/error
"""
try:
# ffprobe命令:获取视频流的编码名称
cmd = [
self.ffprobe_path,
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
# 执行命令并获取输出
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore")
codec = result.stdout.strip().lower()
# 兼容不同命名(h264=avc1,h265=hevc)
if codec in ["h264", "avc1"]:
return "h264"
elif codec in ["h265", "hevc"]:
return "h265"
else:
return "unknown"
except Exception as e:
self._log(f"检测{os.path.basename(video_path)}编码失败:{str(e)}")
return "error"
def _convert_single_video(self, input_path, output_path):
"""
单文件转码(H265→H264)
使用ffmpeg命令行,保留原分辨率/帧率/音频
"""
try:
# FFmpeg转码命令:H265转H264,CRF=23(画质平衡),保留音频
cmd = [
self.ffmpeg_path,
"-i", input_path, # 输入文件
"-c:v", "libx264", # 视频编码器:H264
"-crf", "23", # 画质参数(18-28,值越小画质越好)
"-preset", "medium", # 编码速度/压缩比平衡
"-c:a", "copy", # 音频流直接复制(不重新编码)
"-y", # 覆盖已存在的输出文件
"-hide_banner", # 隐藏banner信息
"-loglevel", "error", # 仅输出错误信息
output_path
]
# 执行转码命令
subprocess.run(cmd, check=True, capture_output=True)
return True
except subprocess.CalledProcessError as e:
self._log(f"转码{os.path.basename(input_path)}失败:{e.stderr[:200]}")
return False
except Exception as e:
self._log(f"转码{os.path.basename(input_path)}异常:{str(e)}")
return False
def _convert_worker(self):
"""转码工作线程(后台运行,不卡界面)"""
source_dir = self.source_dir.get()
output_dir = self.output_dir.get()
# 校验文件夹
if not os.path.exists(source_dir):
messagebox.showerror("错误", "源文件夹不存在!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
if not os.path.exists(output_dir):
messagebox.showerror("错误", "输出文件夹不存在!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
# 获取源文件夹下所有MP4文件
mp4_files = [f for f in os.listdir(source_dir) if f.lower().endswith(".mp4")]
self.total_files = len(mp4_files)
self.processed_files = 0
self.skipped_files = 0
self.failed_files = 0
if self.total_files == 0:
self._log("源文件夹中未找到MP4文件!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
self._log(f"开始处理:共找到{self.total_files}个MP4文件")
# 遍历处理每个MP4文件
for filename in mp4_files:
if not self.is_converting: # 检测是否停止
self._log("转码已手动停止")
break
input_path = os.path.join(source_dir, filename)
output_path = os.path.join(output_dir, filename)
# 检测编码格式
codec = self._get_video_codec(input_path)
if codec == "h264":
# H264文件跳过
self.skipped_files += 1
self._log(f"跳过{filename}(已为H264编码)")
elif codec == "h265":
# H265文件转码
self._log(f"开始转码{filename}(H265→H264)")
success = self._convert_single_video(input_path, output_path)
if success:
self._log(f"转码完成:{filename}")
else:
self.failed_files += 1
else:
# 编码未知/检测失败
self.failed_files += 1
self._log(f"跳过{filename}(编码未知/检测失败)")
# 更新进度
self.processed_files += 1
progress = (self.processed_files / self.total_files) * 100
self.progress_var.set(progress)
self.label_progress.config(
text=f"处理中 | 总文件:{self.total_files} | 已处理:{self.processed_files} | 跳过:{self.skipped_files} | 失败:{self.failed_files}"
)
# 转码完成/停止后重置状态
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
self.label_progress.config(
text=f"处理完成 | 总文件:{self.total_files} | 已处理:{self.processed_files} | 跳过:{self.skipped_files} | 失败:{self.failed_files}"
)
self._log(f"转码结束:成功{self.processed_files - self.skipped_files - self.failed_files} | 跳过{self.skipped_files} | 失败{self.failed_files}")
def _start_convert(self):
"""启动转码(新开线程,避免界面卡死)"""
# 校验文件夹是否选择
if not self.source_dir.get():
messagebox.showwarning("提示", "请先选择源文件夹!")
return
if not self.output_dir.get():
messagebox.showwarning("提示", "请先选择输出文件夹!")
return
# 校验FFmpeg路径是否存在
if not os.path.exists(self.ffmpeg_path):
messagebox.showerror("错误", f"FFmpeg路径不存在:{self.ffmpeg_path}\n请修改代码中的ffmpeg_path配置!")
return
if not os.path.exists(self.ffprobe_path):
messagebox.showerror("错误", f"FFprobe路径不存在:{self.ffprobe_path}\n请修改代码中的ffprobe_path配置!")
return
# 启动转码
self.is_converting = True
self.btn_start.config(state="disabled")
self.btn_stop.config(state="normal")
self.progress_var.set(0)
self._log("启动转码线程...")
# 新开线程执行转码(避免阻塞UI)
self.convert_thread = threading.Thread(target=self._convert_worker, daemon=True)
self.convert_thread.start()
def _stop_convert(self):
"""停止转码"""
self.is_converting = False
self.btn_stop.config(state="disabled")
self._log("正在停止转码,请等待当前文件处理完成...")
if __name__ == "__main__":
# 适配Windows高分屏
if sys.platform == "win32":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
root = tk.Tk()
app = H265ToH264Converter(root) # 修正之前的笔误
root.mainloop()
增加10bit转8bit
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import subprocess
import threading
import time
import sys
from loguru import logger
base_dir = os.path.dirname(__file__)
class H265ToH264Converter(object):
def __init__(self, root):
self.ffmpeg_path = os.path.join(base_dir, 'ffmpeg/bin/ffmpeg.exe')
self.ffprobe_path = os.path.join(base_dir, 'ffmpeg/bin/ffprobe.exe')
self.root = root
self.root.title("视频转码工具")
self.root.geometry("700x450")
# 兼容logo不存在的情况
logo_path = os.path.join(base_dir, 'logo.png')
if os.path.exists(logo_path):
icon_img = tk.PhotoImage(file=logo_path)
self.root.iconphoto(True, icon_img) # True表示应用到所有子窗口
self.root.resizable(False, False)
# 全局变量
self.source_dir = tk.StringVar() # 源文件夹
self.output_dir = tk.StringVar() # 输出文件夹
self.is_converting = False # 是否正在转码
self.convert_thread = None # 转码线程
self.total_files = 0 # 总待处理MP4数
self.processed_files = 0 # 已处理数
self.skipped_files = 0 # 跳过的H264文件数
self.failed_files = 0 # 失败文件数
# 创建UI界面
self._create_widgets()
def _create_widgets(self):
"""创建界面元素(移除Emoji,替换为ASCII/中文符号)"""
# 1. 源文件夹选择区域
frame_source = ttk.LabelFrame(self.root, text="【源文件夹】存放MP4视频")
frame_source.pack(fill="x", padx=20, pady=10)
ttk.Entry(frame_source, textvariable=self.source_dir, state="readonly", width=60).pack(side="left", padx=10,
pady=5)
ttk.Button(frame_source, text="选择文件夹", command=self._select_source_dir).pack(side="left", padx=5, pady=5)
# 2. 输出文件夹选择区域
frame_output = ttk.LabelFrame(self.root, text="【输出文件夹】保存转码后的视频")
frame_output.pack(fill="x", padx=20, pady=10)
ttk.Entry(frame_output, textvariable=self.output_dir, state="readonly", width=60).pack(side="left", padx=10,
pady=5)
ttk.Button(frame_output, text="选择文件夹", command=self._select_output_dir).pack(side="left", padx=5, pady=5)
# 3. 控制按钮区域
frame_control = ttk.Frame(self.root)
frame_control.pack(fill="x", padx=20, pady=10)
self.btn_start = ttk.Button(frame_control, text="开始转码", command=self._start_convert)
self.btn_start.pack(side="left", padx=10, pady=5)
self.btn_stop = ttk.Button(frame_control, text="停止转码", command=self._stop_convert, state="disabled")
self.btn_stop.pack(side="left", padx=10, pady=5)
# 4. 进度显示区域
frame_progress = ttk.LabelFrame(self.root, text="【转码进度】")
frame_progress.pack(fill="both", padx=20, pady=10, expand=True)
# 进度条
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(frame_progress, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill="x", padx=10, pady=5)
# 进度文本
self.label_progress = ttk.Label(frame_progress, text="等待开始 | 总文件:0 | 已处理:0 | 跳过:0 | 失败:0")
self.label_progress.pack(padx=10, pady=5)
# 日志显示框
self.text_log = tk.Text(frame_progress, height=10, width=80)
self.text_log.pack(padx=10, pady=5)
self.text_log.config(state="disabled") # 初始只读
def _select_source_dir(self):
"""选择源文件夹"""
dir_path = filedialog.askdirectory(title="选择存放MP4的源文件夹")
if dir_path:
self.source_dir.set(dir_path)
self._log(f"已选择源文件夹:{dir_path}")
def _select_output_dir(self):
"""选择输出文件夹"""
dir_path = filedialog.askdirectory(title="选择转码后视频的输出文件夹")
if dir_path:
self.output_dir.set(dir_path)
self._log(f"已选择输出文件夹:{dir_path}")
def _log(self, msg):
"""日志输出(带时间戳,移除Emoji)"""
self.text_log.config(state="normal")
timestamp = time.strftime("%H:%M:%S")
# 替换日志中的Emoji为中文符号
msg = msg.replace("✅", "【成功】").replace("❌", "【错误】").replace("⚠️", "【警告】")
msg = msg.replace("🔄", "【转码】").replace("⏭️", "【跳过】").replace("🚀", "【启动】").replace("🛑", "【停止】")
msg = msg.replace("📌", "【提示】").replace("📊", "【统计】")
self.text_log.insert(tk.END, f"[{timestamp}] {msg}\n")
self.text_log.see(tk.END) # 自动滚动到底部
self.text_log.config(state="disabled")
def _get_video_codec(self, video_path):
"""
检测视频编码格式(通过ffprobe命令)
返回:h264/h265/unknown/error
"""
try:
# ffprobe命令:获取视频流的编码名称
cmd = [
self.ffprobe_path,
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
# 执行命令并获取输出
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore")
codec = result.stdout.strip().lower()
# 兼容不同命名(h264=avc1,h265=hevc)
if codec in ["h264", "avc1"]:
return "h264"
elif codec in ["h265", "hevc"]:
return "h265"
else:
return "unknown"
except Exception as e:
self._log(f"检测{os.path.basename(video_path)}编码失败:{str(e)}")
return "error"
def _get_video_bit_depth(self, video_path):
"""
检测视频的色深(bit depth)
返回:8/10/12/unknown/error
"""
try:
# ffprobe命令:获取像素格式和色深信息
cmd = [
self.ffprobe_path,
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=pix_fmt,bit_depth",
"-of", "default=noprint_wrappers=1",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore")
output = result.stdout.lower()
# 方法1:直接读取bit_depth字段(优先)
if "bit_depth=" in output:
bit_depth = output.split("bit_depth=")[1].split("\n")[0].strip()
if bit_depth.isdigit():
return int(bit_depth)
# 方法2:从像素格式推断(兼容老版本ffprobe)
pix_fmt = output.split("pix_fmt=")[1].split("\n")[0].strip() if "pix_fmt=" in output else ""
if "p10" in pix_fmt or "10le" in pix_fmt or "10be" in pix_fmt:
return 10
elif "p12" in pix_fmt or "12le" in pix_fmt or "12be" in pix_fmt:
return 12
elif "p8" in pix_fmt or pix_fmt in ["yuv420p", "nv12", "nv21"]:
return 8
else:
return "unknown"
except Exception as e:
self._log(f"检测{os.path.basename(video_path)}色深失败:{str(e)}")
return "error"
def _convert_single_video(self, input_path, output_path, force_8bit=False):
"""
单文件转码(H265→H264),支持强制转换为8bit
:param input_path: 输入文件路径
:param output_path: 输出文件路径
:param force_8bit: 是否强制转换为8bit
:return: 转码是否成功
"""
try:
# 基础转码命令(修正参数顺序:所有输出参数放在输入之后、输出路径之前)
cmd = [
self.ffmpeg_path,
"-i", input_path, # 输入文件(必须放在最前面)
"-c:v", "libx264", # 视频编码器
"-crf", "23", # 画质参数
"-preset", "medium", # 编码速度
"-hide_banner", # 隐藏banner
"-loglevel", "error", # 日志级别(必须放在参数中间,而非最后)
]
# 如果需要强制转换为8bit,添加像素格式参数(放在视频参数后、音频参数前)
if force_8bit:
cmd.extend(["-pix_fmt", "yuv420p"])
# 音频参数 + 覆盖参数 + 输出路径(最后是输出路径)
cmd.extend([
"-c:a", "copy", # 音频复制
"-y", # 覆盖输出
output_path # 输出路径(必须放在最后)
])
# 执行转码命令(打印命令便于调试)
self._log(f"执行命令:{' '.join(cmd)}")
subprocess.run(cmd, check=True, capture_output=True, encoding="utf-8", errors="ignore")
return True
except subprocess.CalledProcessError as e:
error_info = e.stderr[:200] if e.stderr else "无错误信息"
self._log(f"转码{os.path.basename(input_path)}失败:{error_info}")
return False
except Exception as e:
self._log(f"转码{os.path.basename(input_path)}异常:{str(e)}")
return False
def _convert_worker(self):
"""转码工作线程(后台运行,不卡界面)"""
source_dir = self.source_dir.get()
output_dir = self.output_dir.get()
# 校验文件夹
if not os.path.exists(source_dir):
messagebox.showerror("错误", "源文件夹不存在!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
if not os.path.exists(output_dir):
messagebox.showerror("错误", "输出文件夹不存在!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
# 获取源文件夹下所有MP4文件
mp4_files = [f for f in os.listdir(source_dir) if f.lower().endswith(".mp4")]
self.total_files = len(mp4_files)
self.processed_files = 0
self.skipped_files = 0
self.failed_files = 0
if self.total_files == 0:
self._log("源文件夹中未找到MP4文件!")
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
return
self._log(f"开始处理:共找到{self.total_files}个MP4文件")
# 遍历处理每个MP4文件
for filename in mp4_files:
if not self.is_converting: # 检测是否停止
self._log("转码已手动停止")
break
input_path = os.path.join(source_dir, filename)
output_path = os.path.join(output_dir, filename)
# 检测编码格式
codec = self._get_video_codec(input_path)
if codec == "h264":
# 检测H264文件是否为8bit,如果不是也转8bit
bit_depth = self._get_video_bit_depth(input_path)
if bit_depth == 8:
# H264+8bit 文件跳过
self.skipped_files += 1
self._log(f"跳过{filename}(已为H264+8bit编码)")
else:
# H264但非8bit,需要转8bit
self._log(f"开始处理{filename}(H264非8bit→H264+8bit)")
success = self._convert_single_video(input_path, output_path, force_8bit=True)
if success:
self._log(f"转码完成:{filename}(已转换为8bit)")
else:
self.failed_files += 1
elif codec == "h265":
# 检测H265文件的色深
bit_depth = self._get_video_bit_depth(input_path)
self._log(f"检测{filename}:H265编码,色深={bit_depth}bit")
# 判断是否需要强制转8bit
force_8bit = bit_depth != 8 and bit_depth != "error" and bit_depth != "unknown"
convert_note = "(同时转换为8bit)" if force_8bit else ""
self._log(f"开始转码{filename}(H265→H264){convert_note}")
success = self._convert_single_video(input_path, output_path, force_8bit=force_8bit)
if success:
self._log(f"转码完成:{filename} {convert_note}")
else:
self.failed_files += 1
else:
# 编码未知/检测失败
self.failed_files += 1
self._log(f"跳过{filename}(编码未知/检测失败)")
# 更新进度
self.processed_files += 1
progress = (self.processed_files / self.total_files) * 100
self.progress_var.set(progress)
self.label_progress.config(
text=f"处理中 | 总文件:{self.total_files} | 已处理:{self.processed_files} | 跳过:{self.skipped_files} | 失败:{self.failed_files}"
)
# 转码完成/停止后重置状态
self.is_converting = False
self.btn_start.config(state="normal")
self.btn_stop.config(state="disabled")
self.label_progress.config(
text=f"处理完成 | 总文件:{self.total_files} | 已处理:{self.processed_files} | 跳过:{self.skipped_files} | 失败:{self.failed_files}"
)
self._log(
f"转码结束:成功{self.processed_files - self.skipped_files - self.failed_files} | 跳过{self.skipped_files} | 失败{self.failed_files}")
def _start_convert(self):
"""启动转码(新开线程,避免界面卡死)"""
# 校验文件夹是否选择
if not self.source_dir.get():
messagebox.showwarning("提示", "请先选择源文件夹!")
return
if not self.output_dir.get():
messagebox.showwarning("提示", "请先选择输出文件夹!")
return
# 校验FFmpeg路径是否存在
if not os.path.exists(self.ffmpeg_path):
messagebox.showerror("错误", f"FFmpeg路径不存在:{self.ffmpeg_path}\n请修改代码中的ffmpeg_path配置!")
return
if not os.path.exists(self.ffprobe_path):
messagebox.showerror("错误", f"FFprobe路径不存在:{self.ffprobe_path}\n请修改代码中的ffprobe_path配置!")
return
# 启动转码
self.is_converting = True
self.btn_start.config(state="disabled")
self.btn_stop.config(state="normal")
self.progress_var.set(0)
self._log("启动转码线程...")
# 新开线程执行转码(避免阻塞UI)
self.convert_thread = threading.Thread(target=self._convert_worker, daemon=True)
self.convert_thread.start()
def _stop_convert(self):
"""停止转码"""
self.is_converting = False
self.btn_stop.config(state="disabled")
self._log("正在停止转码,请等待当前文件处理完成...")
if __name__ == "__main__":
# 适配Windows高分屏
if sys.platform == "win32":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
root = tk.Tk()
app = H265ToH264Converter(root)
root.mainloop()
更多推荐



所有评论(0)