我将关于“Bink究竟是什么东西”之类的偏概念类的东西写到了本文的末尾
因为Epic的官方工具Rad GAME Tools并不支持将 .bik文件直接转换成MP4文件,所以我们在转换视频的时候只能先通过官方工具将 .bik 文件转换成 .avi 然后再通过其他诸如格式工厂、PR之类的软件将其转换成mp4。
为什么不直接使用avi文件呢,反正avi文件也是所有视频播放软件都必须适配的文件格式?
因为avi文件的体积太大了,让我们举个例子。 现在我们手里有一个以bink2格式压缩的 .bik文件(文件体积大约为27MB),我们如果将其直接转换成 .avi 文件,其最终的体积将来到恐怖的3.1GB
而且官方工具并不支持你使用多线程对文件进行格式转换,这在效率上让人感觉有些尴尬 :(
直接进入代码参考模式
首先可以确定的是,我只在python上做到了这一点(尽管可能也许这套代码在C++或rust上的效率会更高)
首先我们需要为代码引入需要使用的python第三方库。如果你不想看我咬言砸字,可以直接跳到后面看最终的代码
import ctypes
from ctypes import Structure, POINTER, c_int, c_uint, c_ulong, c_char_p, c_void_p, byref
import numpy as np
import av
一般情况下你的python可能会确实numpy 和 av这个库,所以你可以直接使用pip安装这两个库
pip install numpy
pip install av
安装好后我们需要为代码引入dll,这个文件你可以直接在apex的根目录中找到 -> bink2w64.dll, 然后再代码中定义我们需要使用的函数、结构体之类的东西
bink_dll = ctypes.CDLL('./bink2w64.dll')
BINK_SURFACE32RGBA = 6
# Bink结构体
class BINKRECT(Structure):
_fields_ = [
("Left", c_int),
("Top", c_int),
("Width", c_int),
("Height", c_int)
]
class BINK(Structure):
_fields_ = [
("Width", c_int),
("Height", c_int),
("Frames", c_uint),
("FrameNum", c_uint),
("FrameRate", c_uint),
("FrameRateDiv", c_uint),
("ReadError", c_uint),
("OpenFlags", c_ulong),
("FrameRects", BINKRECT),
("NumRects", c_uint),
("FrameChangePercent", c_uint)
]
# Bink的签名
bink_dll.BinkOpen.argtypes = [c_char_p, c_ulong]
bink_dll.BinkOpen.restype = c_void_p
bink_dll.BinkClose.argtypes = [c_void_p]
bink_dll.BinkDoFrame.argtypes = [c_void_p]
bink_dll.BinkNextFrame.argtypes = [c_void_p]
bink_dll.BinkCopyToBuffer.argtypes = [c_void_p, ctypes.POINTER(ctypes.c_ubyte), c_int, c_uint, c_uint, c_uint, c_ulong]
bink_dll.BinkWait.argtypes = [c_void_p]
bink_dll.BinkWait.restype = c_int
至此,你的代码已经初具加载.bik文件的能力了。
以下是关于部分bink函数的解释
BinkOpen -> 打开.bik文件
参数: 1. bik文件的地址(相对、绝对路径都可以) 2. flag(默认输入0即可)
返回值: 函数指针
BinkClose -> 关闭bik文件
BinkDoFrame -> 开始处理当前帧的数据(只有图像数据)
BinkNextFrame -> 让指针前进到下一帧,以供↑获取新的数据
BinkCopyToBuffer -> 将帧数据复制到你定义的Buffer里
参数: 1. bink指针 2. Buffer 3. Width * 4 4. Height 5. 0(起始X轴坐标) 6. 0(起始Y轴坐标) 7. bink常量(apex使用的常量默认为6,即已经定义的变量BINK_SURFACE32RGBA)
再然后就到了处理bink的数据然后写入到mp4文件里了
# 打开 Bink 文件
def open_bink(file_path):
bink = bink_dll.BinkOpen(file_path.encode('utf-8'), 0)
if not bink:
raise RuntimeError("Failed to open Bink file.")
return bink
# 播放 Bink 视频
def play_bink(file_path):
"""
:param file_path: E:\\APEX\\media\\frontend.bik
:return:
"""
# 创建bik文件指针
bink = open_bink(file_path)
# 获取文件.bik文件名并变更为.mp4后缀
output_file_name = file_path.split('\\')[-1].replace('.bik','.mp4')
try:
# 获取视频信息 结构参考class BINK
bink_info = BINK()
ctypes.memmove(ctypes.byref(bink_info), bink, ctypes.sizeof(BINK))
width, height = bink_info.Width, bink_info.Height
# 创建帧缓冲区
buffer_size = width * height * 4 # RGBA
buffer = (ctypes.c_ubyte * buffer_size)()
# 创建容器
container = av.open(output_file_name, mode='w')
# 定义编码格式以及其他的东西, 帧数默认为30帧,几乎不会变
stream = container.add_stream("h264", rate=30)
stream.width = width
stream.height = height
stream.pix_fmt = "yuv420p"
# 开始将数据写入容器
while bink_dll.BinkWait(bink) == 0:
bink_dll.BinkDoFrame(bink)
bink_dll.BinkCopyToBuffer(bink, buffer, width * 4, height, 0, 0, BINK_SURFACE32RGBA)
# 将缓冲区转换为图像
frame_data = np.frombuffer(buffer, dtype=np.uint8).reshape((height, width, 4))
rgb_frame = frame_data[:, :, :3]
frame_av = av.VideoFrame.from_ndarray(rgb_frame, format="rgb24")
for packet in stream.encode(frame_av):
container.mux(packet)
# img = Image.fromarray(frame_data)
# img.show()
bink_dll.BinkNextFrame(bink)
for packet in stream.encode(None): # 刷新缓冲区
container.mux(packet)
container.close()
finally:
bink_dll.BinkClose(bink)
然后写完以上代码后你就可以通过调用play_bink来把.bik文件转为mp4文件了
play_bink("E:\\APEX\\media\\frontend.bik")
完整的代码参考
import ctypes
from ctypes import Structure, POINTER, c_int, c_uint, c_ulong, c_char_p, c_void_p, byref
import numpy as np
import av
# 加载 Bink 动态库
bink_dll = ctypes.CDLL('./bink2w64.dll')
#ctypes.LibraryLoader
# 定义 Bink 的常量
BINK_SURFACE32RGBA = 6
# 定义结构体
class BINKRECT(Structure):
_fields_ = [
("Left", c_int),
("Top", c_int),
("Width", c_int),
("Height", c_int)
]
class BINK(Structure):
_fields_ = [
("Width", c_int),
("Height", c_int),
("Frames", c_uint),
("FrameNum", c_uint),
("FrameRate", c_uint),
("FrameRateDiv", c_uint),
("ReadError", c_uint),
("OpenFlags", c_ulong),
("FrameRects", BINKRECT),
("NumRects", c_uint),
("FrameChangePercent", c_uint)
]
# 定义 Bink 函数的签名
bink_dll.BinkOpen.argtypes = [c_char_p, c_ulong]
bink_dll.BinkOpen.restype = c_void_p
bink_dll.BinkClose.argtypes = [c_void_p]
bink_dll.BinkDoFrame.argtypes = [c_void_p]
bink_dll.BinkNextFrame.argtypes = [c_void_p]
bink_dll.BinkCopyToBuffer.argtypes = [c_void_p, ctypes.POINTER(ctypes.c_ubyte), c_int, c_uint, c_uint, c_uint, c_ulong]
bink_dll.BinkWait.argtypes = [c_void_p]
bink_dll.BinkWait.restype = c_int
# 打开 Bink 文件
def open_bink(file_path):
bink = bink_dll.BinkOpen(file_path.encode('utf-8'), 0)
if not bink:
raise RuntimeError("Failed to open Bink file.")
return bink
# 播放 Bink 视频
def play_bink(file_path):
"""
:param file_path: E:\\APEX\\media\\frontend.bik
:return:
"""
# 创建bik文件指针
bink = open_bink(file_path)
# 获取文件.bik文件名并变更为.mp4后缀
output_file_name = file_path.split('\\')[-1].replace('.bik','.mp4')
try:
# 获取视频信息 结构参考class BINK
bink_info = BINK()
ctypes.memmove(ctypes.byref(bink_info), bink, ctypes.sizeof(BINK))
width, height = bink_info.Width, bink_info.Height
# 创建帧缓冲区
buffer_size = width * height * 4 # RGBA
buffer = (ctypes.c_ubyte * buffer_size)()
# 创建容器
container = av.open(output_file_name, mode='w')
# 定义编码格式以及其他的东西, 帧数默认为30帧,几乎不会变
stream = container.add_stream("h264", rate=30)
stream.width = width
stream.height = height
stream.pix_fmt = "yuv420p"
# 开始将数据写入容器
while bink_dll.BinkWait(bink) == 0:
bink_dll.BinkDoFrame(bink)
bink_dll.BinkCopyToBuffer(bink, buffer, width * 4, height, 0, 0, BINK_SURFACE32RGBA)
# 将缓冲区转换为图像
frame_data = np.frombuffer(buffer, dtype=np.uint8).reshape((height, width, 4))
rgb_frame = frame_data[:, :, :3]
frame_av = av.VideoFrame.from_ndarray(rgb_frame, format="rgb24")
for packet in stream.encode(frame_av):
container.mux(packet)
# img = Image.fromarray(frame_data)
# img.show()
bink_dll.BinkNextFrame(bink)
for packet in stream.encode(None): # 刷新缓冲区
container.mux(packet)
container.close()
finally:
bink_dll.BinkClose(bink)
# 示例:播放 Bink 视频
play_bink(r"E:\\APEX\\media\\frontend.bik")
你可以使用threading和sqllite等库让代码自动判断那些bik文件是新的,然后批量转为mp4
比如:
from pathlib import Path
import threading
# 此处省略12390127349128行上文代码
def get_all_file_paths(directory):
path = Path(directory)
return [str(file) for file in path.rglob("*") if file.is_file() and str(file).endswith("bik")]
paths = get_all_file_paths(r"E:\\Apex\\media")
threads = []
for bink_path in paths:
t = threading.Thread(
target=play_bink,
args=(bink_path,)
)
t.start()
threads.append(t)
for t in threads:
t.join()
概念类:什么是bink?
Bink 是 RAD Game Tools 提供的一套高性能音视频编解码解决方案。它被广泛应用于游戏领域(2020 年已有约 500 款游戏采用了 Bink 视频编解码器),支持多种平台。Bink 技术具有高压缩比与低解码开销的特点:例如 Bink 音频编解码器可实现约 10:1 的感知无损压缩,其解码速度接近 ADPCM,同时借助 GPU 加速时,Bink 视频解码 4K 帧只需不到 1 毫秒,内存占用也非常低。
Apex Legends 第 22 赛季的音频编解码升级
在 Apex Legends 第 22 赛季中,开发团队对音频系统进行了重大升级。此前游戏使用的是 Bink Audio 2 编解码器,但社区中存在脚步声不清晰、音频偶尔缺失等问题。为此,重生工作室在这一版本中将旧版编解码器替换为 RAD 最新的 BinkAudioCodec(该技术根据虚幻引擎路线图在 UE5.5 预览版中首次引入),以解决持续存在的音频稳定性和性能问题。
迁移动机:玩家长期反映竞争环境中音频指示(如脚步声)不够可靠,影响游戏体验。采用新版编解码器有望带来更好的音频质量和一致性。
过程与来源:BinkAudioCodec 是 RAD Game Tools 最新的音频编解码技术,在 UE5.5中作为内置功能首次面世。Apex开发团队根据这一技术,构建了自定义分支,将其集成到自家的弗兰肯斯坦引擎中。
定制优化:为了充分发挥新编解码器的优势,重生工作室还基于BinkAudioCodec创建了自己的分支,针对 Apex 的使用场景(例如海量并发音源)进行专项优化,使其性能与稳定性更符合游戏需求。