大鱼自制透明通道视频压缩器(最终更新版 有bug也不修了)

软件下载

下载后解压拷贝到应用程序文件夹。m芯片系统版本低于14的也下Intel版

开发者手记:由于只有苹果系统自带底层框架videotoolbox才能使用GPU加速输出带通道的HEVC文件,该底层工具只支持M芯片完美加速渲染。Intel版本只能采用速度巨慢的libx265软件编解码,这也是无奈之举。由于FCPX也是采用的videotoolbox工具进行转码和渲染 所以Intel电脑在FCPX使用上效率也大不如M芯片电脑。 从系统底层剖析 建议影视从业者不要再采购Intel Mac。顺带附上Intel版本未编译原文件https://dayumedia2.feishu.cn/docx/AwxddCRuUoRmCxx8QoWcpjMVnVh?from=from_copylink

软件主要功能

众所周知 我们要从AE导出带通道的视频素材只能选择PRORES4444 体积非常巨大 所以我做了一个压缩器 专门针对带通道素材的压缩

另外有些人导出时忘记勾选Alpha 导致输出带黑背景 这里也做了一个简单的黑色背景去除器 但是容差值默认很低 只能去除纯黑色 那种带渐变带反光眩光的无法去黑

用法很简单 将一个或者多个素材拖到界面中就自动处理 导出的编码为HEVC with Alpha,大大减小了体积

同时自动识别 如果导入的素材不带通道 那么就会按照预设码率压缩为普通HEVC视频,如果导入的素材带有透明通道,那么在压缩成HEVC的同时还会保留透明通道。输出路径默认为原文件旁边的HEVC Alpha已转换文件夹

新增的转代理功能也不错 转码速度不输FCPX,但是体积比FCPX的ProRes proxy代理要小很多。输出路径默认为原文件旁边的Proxy Media文件夹

由于只有苹果的videotoolbox能够输出HEVC with Alpha,所以会有很多格式的视频不支持处理,例如MKV avi等,只要导入FCPX不识别的 丢到这个软件也无法处理

8.18更新 新增FFMPEG版本指示器以及必要的编码器加载情况 如果打开显示红色disable则本软件无法使用,显示绿色Enable该软件才能正常使用

以下是本程序M芯片版本Python3源代码,如有大神编译成其他版本或者修复改进 请发我一份感激不尽。

邮箱 luozhong@mrdayumedia.com

import os
import sys
import subprocess
import threading
import tkinter as tk
from tkinterdnd2 import DND_FILES, TkinterDnD
import ffmpeg
from tkinter import ttk, filedialog, messagebox, scrolledtext

def get_ffmpeg_path():
if getattr(sys, 'frozen', False): # 判断是否打包
base_path = sys._MEIPASS # 打包后临时文件目录
resources_path = os.path.join(base_path, "resources")
ffmpeg_executable = os.path.join(resources_path, "ffmpeg")
ffprobe_executable = os.path.join(resources_path, "ffprobe")
else:
ffmpeg_executable = '/opt/homebrew/Cellar/ffmpeg/7.0.2/bin/ffmpeg' # 你的 FFmpeg 安装路径
ffprobe_executable = '/opt/homebrew/Cellar/ffmpeg/7.0.2/bin/ffprobe' # 你的 FFprobe 安装路径

version_output = subprocess.check_output([ffmpeg_executable, '-version']).decode('utf-8')
return ffmpeg_executable, ffprobe_executable, version_output

def check_videotoolbox_enabled(version_output):
return '--enable-videotoolbox' in version_output

def get_video_info(video_path, ffprobe_path):
probe = ffmpeg.probe(video_path, cmd=ffprobe_path)
video_info = {}

for stream in probe['streams']:
if stream['codec_type'] == 'video':
video_info = {
'codec_name': stream['codec_name'],
'pix_fmt': stream['pix_fmt'],
}
break

return video_info

class VideoConverterApp:
def __init__(self, root):
self.root = root
self.root.title("大鱼自制HEVC带通道视频转换器")
self.root.geometry("600x800")

self.ffmpeg_path, self.ffprobe_path, version_output = get_ffmpeg_path()
self.videotoolbox_enabled = check_videotoolbox_enabled(version_output)

ffmpeg_info_label = tk.Label(root, text=f"Using FFmpeg version:\n{version_output.splitlines()[0]}")
ffmpeg_info_label.pack(pady=5)

videotoolbox_status = "Enabled" if self.videotoolbox_enabled else "Disabled"
self.videotoolbox_label = tk.Label(root, text=f"Videotoolbox: {videotoolbox_status}", fg="green" if self.videotoolbox_enabled else "red")
self.videotoolbox_label.pack(pady=5)

self.drop_area = tk.Label(root, text="+将视频文件或文件夹拖放到此处+", bg="#2e2e2e", fg="white", width=50, height=5)
self.drop_area.pack(pady=20)
self.drop_area.drop_target_register(DND_FILES)
self.drop_area.dnd_bind('<<Drop>>', self.on_drop)

self.select_button = tk.Button(root, text="+选择文件夹+", command=self.select_working_directory)
self.select_button.pack(pady=10)

self.bitrate_frame = tk.Frame(root)
self.bitrate_frame.pack(pady=5)

self.bitrate_label = tk.Label(self.bitrate_frame, text="设置码率:")
self.bitrate_label.pack(side=tk.LEFT)

self.bitrate_entry = tk.Entry(self.bitrate_frame, width=10)
self.bitrate_entry.insert(0, "10")
self.bitrate_entry.pack(side=tk.LEFT)

self.bitrate_unit_label = tk.Label(self.bitrate_frame, text="M")
self.bitrate_unit_label.pack(side=tk.LEFT)

self.bitrate_entry.config(validate="key", validatecommand=(self.bitrate_entry.register(self.validate_input), "%P"))

self.convert_black_to_transparent = tk.BooleanVar()
self.check_button = tk.Checkbutton(root, text="将黑色背景转换为透明(眩光泛光发光素材无法使用)", variable=self.convert_black_to_transparent)
self.check_button.pack(pady=10)

self.overwrite_original = tk.BooleanVar()
self.overwrite_check_button = tk.Checkbutton(root, text="覆盖原文件(请确保素材有备份 生成代理请勿勾选此项)", variable=self.overwrite_original)
self.overwrite_check_button.pack(pady=10)

self.start_button = tk.Button(root, text="生成HEVC with alpha压缩通道媒体", command=self.start_conversion, font=("Helvetica", 16, "bold"))
self.start_button.pack(pady=10)

self.file_progress_bar = ttk.Progressbar(root, orient="horizontal", length=500, mode="indeterminate")
self.file_progress_bar.pack(pady=5)
self.file_progress_bar.pack_forget()

self.proxy_button = tk.Button(root, text="生成ProRes Proxy代理媒体", command=self.generate_prores_proxy, font=("Helvetica", 14))
self.proxy_button.pack(pady=6)

self.console_output = scrolledtext.ScrolledText(root, width=80, height=10, wrap=tk.WORD)
self.console_output.pack(pady=10)

self.info_label = tk.Label(root, text="抖音和B站同ID:大鱼先森Media 有bug请私信反馈 官方网站dayu.media", fg="yellow")
self.info_label.pack(side=tk.BOTTOM, pady=10)

def generate_prores_proxy(self):
file_paths = list(self.file_info.keys())
if not file_paths:
messagebox.showwarning("提示", "请先添加视频文件")
return

self.console_output.delete(1.0, tk.END)
threading.Thread(target=self.process_prores_proxy, args=(file_paths,)).start()

def process_prores_proxy(self, file_paths):
self.file_progress_bar.pack(pady=5)
self.file_progress_bar.start()
self.console_output.insert(tk.END, "正在生成ProRes Proxy,请稍候...\n")
self.console_output.see(tk.END)

for index, file_path in enumerate(file_paths):
if os.path.isfile(file_path):
self.current_file_index = index
self.convert_to_prores_proxy(file_path)

self.file_progress_bar.stop()
self.file_progress_bar.pack_forget()
self.console_output.insert(tk.END, "所有Proxy文件生成完成\n")
self.console_output.see(tk.END)

def convert_to_prores_proxy(self, input_file):
original_dir = os.path.dirname(input_file)
original_filename = os.path.basename(input_file)

# 保证输出文件的扩展名为 .mov
output_file = os.path.splitext(original_filename)[0] + ".mov"
output_file = os.path.join(original_dir, "Proxy Media", output_file)

os.makedirs(os.path.dirname(output_file), exist_ok=True)

try:
command = [
self.ffmpeg_path,
'-i', input_file,
'-c:v', 'prores_videotoolbox',
'-profile:v', '0', # ProRes Proxy
'-pix_fmt', 'p210le', # 使用 p210le 作为像素格式
'-vf', 'scale=1280:-1', # 缩放视频分辨率
'-tag:v', 'apco', # ProRes Proxy format identifier
'-c:a', 'pcm_s16le', # 使用 16 位 PCM 音频格式
'-f', 'mov', # 指定输出格式为 MOV
output_file
]

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 实时输出控制台信息
for stdout_line in iter(process.stdout.readline, ""):
self.console_output.insert(tk.END, stdout_line)
self.console_output.see(tk.END)
self.console_output.update()

for stderr_line in iter(process.stderr.readline, ""):
self.console_output.insert(tk.END, stderr_line)
self.console_output.see(tk.END)
self.console_output.update()

process.stdout.close()
process.stderr.close()
process.wait()

self.console_output.insert(tk.END, f"正在生成ProRes Proxy: {input_file}\n")
self.console_output.insert(tk.END, f"输出文件: {output_file}\n")
self.console_output.see(tk.END)

except Exception as e:
self.console_output.insert(tk.END, f"发生错误: {str(e)}\n")
self.console_output.see(tk.END)

def validate_input(self, input):
if input.isdigit() or input == "":
return True
else:
return False

def on_drop(self, event):
# 调试输出,确认事件触发
print(f"拖放数据: {event.data}")
# 解析和处理拖放的数据
file_paths = self.parse_file_paths(event.data)
if file_paths:
self.display_files_and_info(file_paths)
else:
print("未找到有效文件")

def select_working_directory(self):
directory = filedialog.askdirectory()
if directory:
file_paths = [os.path.join(root, file) for root, _, files in os.walk(directory) for file in files if file.endswith(('.mp4', '.mov', '.m4v'))]
self.display_files_and_info(file_paths)

def parse_file_paths(self, data):
# 清理路径字符串并分割成单独的路径
cleaned_data = data.strip('{}')
file_paths = cleaned_data.split('} {')
valid_files = []

for path in file_paths:
path = path.strip()
if os.path.isfile(path):
if path.lower().endswith(('.mp4', '.mov', '.m4v')):
valid_files.append(path)
elif os.path.isdir(path):
for root, _, files in os.walk(path):
for file in files:
if file.lower().endswith(('.mp4', '.mov', '.m4v')):
valid_files.append(os.path.join(root, file))

# 调试输出解析后的文件路径
print(f"解析后的文件路径: {valid_files}")
return valid_files

def display_files_and_info(self, file_paths):
self.console_output.delete(1.0, tk.END) # 清空文本框
self.file_info = {}

for file_path in file_paths:
video_info = get_video_info(file_path, self.ffprobe_path)
if video_info:
transparency_info = "带Alpha透明通道" if 'a' in video_info['pix_fmt'] else "没有透明通道"
display_text = f"{os.path.basename(file_path)} - {video_info['codec_name']} / {transparency_info}"
self.console_output.insert(tk.END, display_text + "\n")
self.console_output.see(tk.END)
self.file_info[file_path] = video_info

def start_conversion(self):
file_paths = list(self.file_info.keys())
if not file_paths:
messagebox.showwarning("提示", "请先添加视频文件")
return

self.console_output.delete(1.0, tk.END) # 清空文本框
threading.Thread(target=self.process_videos, args=(file_paths,)).start()

def process_videos(self, file_paths):
self.file_progress_bar.pack(pady=5)
self.file_progress_bar.start()
self.console_output.insert(tk.END, "后台正在处理,请稍候...\n")
self.console_output.see(tk.END)

for index, file_path in enumerate(file_paths):
if os.path.isfile(file_path):
self.current_file_index = index
self.convert_video(file_path)

self.file_progress_bar.stop()
self.file_progress_bar.pack_forget()
self.console_output.insert(tk.END, "所有文件处理完成\n")
self.console_output.see(tk.END)

def convert_video(self, input_file):
original_dir = os.path.dirname(input_file)
original_filename = os.path.basename(input_file)

# 始终生成临时文件
temp_output_file = os.path.join(original_dir, f"temp_{original_filename}")

if self.overwrite_original.get():
output_file = input_file # 最终替换原文件
else:
# 如果不覆盖原文件,则生成新的文件名和路径
output_dir = os.path.join(original_dir, "HEVC alpha已转换")
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, original_filename)

bitrate = self.bitrate_entry.get() + "M"
video_info = self.file_info[input_file]

try:
# 如果用户选择了将黑色背景转换为透明,则始终输出带透明通道的视频
if self.convert_black_to_transparent.get():
video_out_args = {
'vcodec': 'hevc_videotoolbox',
'b:v': bitrate,
'tag:v': 'hvc1',
'alpha_quality': 0.9,
'pix_fmt': 'bgra',
'vf': "[0:v]colorkey=0x000000:0.1[ckout]"
}
else:
# 如果视频带有透明通道,则保留透明通道输出
if 'a' in video_info['pix_fmt']:
video_out_args = {
'vcodec': 'hevc_videotoolbox',
'b:v': bitrate,
'tag:v': 'hvc1',
'alpha_quality': 0.9,
'pix_fmt': 'bgra'
}
else:
# 普通视频输出
video_out_args = {
'vcodec': 'hevc_videotoolbox',
'b:v': bitrate,
'tag:v': 'hvc1',
'c:a': 'aac',
'movflags': '+faststart',
'strict': 'experimental'
}

self.console_output.insert(tk.END, f"正在处理文件: {input_file}\n")
self.console_output.insert(tk.END, f"生成临时文件: {temp_output_file}\n")
self.console_output.see(tk.END)

# 先生成临时文件
process = subprocess.Popen(
[self.ffmpeg_path, '-i', input_file] +
sum(([f'-{k}', str(v)] for k, v in video_out_args.items()), []) +
[temp_output_file],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)

for stdout_line in iter(process.stdout.readline, ""):
self.console_output.insert(tk.END, stdout_line)
self.console_output.see(tk.END)
self.console_output.update()

for stderr_line in iter(process.stderr.readline, ""):
self.console_output.insert(tk.END, stderr_line)
self.console_output.see(tk.END)
self.console_output.update()

process.stdout.close()
process.stderr.close()
process.wait()

# 如果覆盖原文件,替换原文件为临时文件
if self.overwrite_original.get():
os.replace(temp_output_file, output_file)
self.console_output.insert(tk.END, f"覆盖原文件: {output_file}\n")
else:
# 如果不覆盖原文件,则将临时文件保存到指定输出路径
os.replace(temp_output_file, output_file)
self.console_output.insert(tk.END, f"保存文件到: {output_file}\n")

except ffmpeg.Error as e:
self.console_output.insert(tk.END, f"发生错误: {e.stderr.decode('utf-8')}\n")
self.console_output.see(tk.END)
finally:
# 清理临时文件(如果存在且没有被替换)
if os.path.exists(temp_output_file) and not self.overwrite_original.get():
os.remove(temp_output_file)

if __name__ == "__main__":
root = TkinterDnD.Tk()
app = VideoConverterApp(root)
root.mainloop()

打包代码如下 依赖库路径请替换为你电脑上的实际路径,2.py替换为你实际的py脚本文件名

pyinstaller --onefile --windowed --add-data "tkinterdnd2:tkinterdnd2" --hidden-import=tkinterdnd2 --add-binary "/opt/homebrew/Cellar/ffmpeg/7.0.2/bin/ffmpeg:resources" --add-binary "/opt/homebrew/Cellar/ffmpeg/7.0.2/bin/ffprobe:resources" --add-binary "libtkdnd2.9.4.dylib:resources" 2.py

大鱼先森粉丝福利

立即查看 了解详情