记一次ffmpeg延迟问题排查
文章目录
- 背景
- 问题代码
- 缩小问题范围
- 链路耗时
- Ffmpeg 耗时
- 问题复现
- Probesize
- 含义
- 作用
- 注意事项
- 尝试解决
- H.264 视频帧技术简介
- 1. H.264 视频帧类型
- (1) I 帧(Intra Frame / Key Frame)
- (2) P 帧(Predictive Frame)
- (3) B 帧(Bi-directional Frame)
- 2. 典型H.264码流结构
- 解决方案1: 减少线程数
- 解决方案2: 指定成切片级多线程
- 效果
背景
最近需要使用ffmpeg实时解码h264视频帧,转换成单帧的图片供前端直接可视化。在使用过程中发现前端显示的图像一直有1-2s的延迟。
问题代码
self.processes[topic] = subprocess.Popen(['ffmpeg', '-i', 'pipe:0', '-f', 'rawvideo', '-pix_fmt', 'bgr24', '-s', '320x180', 'pipe:1'],stdin=subprocess.PIPE, stdout=subprocess.PIPE)
# ...
self.processes[topic].stdin.write(msg.data)
self.processes[topic].stdin.flush()
# ...
def get_frame(self, topic):self.frames[topic] = None# 过滤没消息发出的topicrlist, _, _ = select.select([self.processes[topic].stdout], [], [], 0.01)if rlist:raw_frame = self.processes[topic].stdout.read(320 * 180 * 3)else:# logger.info(f"{topic} no messages")raw_frame = Noneif raw_frame:self.frames[topic] = np.frombuffer(raw_frame, np.uint8).reshape((180, 320, 3))return self.frames[topic]
缩小问题范围
链路耗时
首先第一个怀疑点是链路耗时较长。通过打印链路上各个关键节点的时间,包括摄像头采集的时间、转码成h264视频帧并发出的时间、接收到h264视频帧的时间,统计结果显示从源头到输入ffmpeg管道之前耗时大约只有200ms左右,因此怀疑点瞬间变成了ffmpeg。
Ffmpeg 耗时
ffmpeg处理h264视频帧这么慢的吗?因为ffmpeg管道的输入输出耗时不太好统计,我们索性直接准备一堆h264文件,在本地使用ffmpeg命令批量转换看看。
可以看到80帧的h264文件转成png图片只花了3.24秒,很明显ffmpeg处理的效率是很高的。
结合实时的现象,从感觉上来看像是有图像数据堵塞在了ffmpeg管道中,因为我们实时处理时是支持暂停的,我们把摄像头数据暂停后,发现ffmpeg管道出来的最后一帧图片并不是实际摄像头暂停时的图像,这也能进一步验证我们的想法,即是不是有图像数据堵塞在了ffmpeg管道中,从而在肉眼看起来像是有延迟,而且延迟也不会累积,说明链路上的耗时是能满足要求的。
由于实时渲染图像不方便调试,我们基于以上的思路,将实时处理搬运到本地,做离线处理看看是否能复现问题。
问题复现
h264_files = [f for f in os.listdir(H264_DIR) if f.endswith('.h264')]
ffmpeg_cmd = ['ffmpeg','-f', 'h264', "-loglevel", "debug", # 启用调试日志'-i', "pipe:0" , "-s", "320x180", '-f', 'image2', os.path.join(IMAGE_DIR, "frame_%04d.png")
]ffmpeg_process = subprocess.Popen(ffmpeg_cmd,stdin=subprocess.PIPE)try:start_time = time.time()for idx, h264_file in enumerate(h264_files):png_output = 'frame_{:04d}.png'.format(idx + 1)h264_path = os.path.join(H264_DIR, h264_file)print(f"Processing file: {h264_file} save to {png_output}")with open(h264_path, "rb") as f:while True:data = f.read(1024 * 1024) # 读取1MB的数据块if not data:break# print("Writing data to ffmpeg process.")ffmpeg_process.stdin.write(data)time.sleep(1)
except Exception as e:print(f"Error processing file: {h264_file}")print(e)
finally:# 关闭FFmpeg进程ffmpeg_process.stdin.close()ffmpeg_process.wait()print(f"Processing time: {time.time() - start_time}s")
print("Frames saved as PNG images.")
通过打印日志,发现很容易能复现出来,而且问题更加严重,在ffmpeg已经积累了33帧左右h264文件时,image才输出保存到目录中。
再分析分析日志, 好像发现了端倪:
在输出图像之前,有这样一行日志打印,查阅资料看看这个 probesize是什么意思?
Probesize
ffmpeg
中的 probesize
参数用于控制**初始分析阶段**读取的数据量,以探测输入文件的基本信息(如格式、流数据等)。
含义
-
probesize
是一个数值参数,单位是**字节(bytes)**,默认值通常为5,000,000
字节(约5MB)。 -
它定义了
ffmpeg
在开始处理输入文件时,最多读取多少数据来检测文件的容器格式、流信息(如视频、音频、字幕等)和其他元数据。
作用
-
加速分析过程
-
通过限制初始读取的数据量,避免
ffmpeg
无谓地扫描整个大文件(尤其是网络流或大型文件),从而加快分析速度。 -
例如,对于远程直播流,设置较小的
probesize
可以更快地进入实际处理阶段。
-
-
处理不完整的文件或特殊格式
-
某些文件(如损坏的或未完全下载的媒体)可能包含无效的头部信息。调整
probesize
可以强制ffmpeg
在更早或更晚的位置检测格式。 -
对于某些非标准格式(如无明确头部信息的流),可能需要增加
probesize
以确保正确识别。
-
-
平衡准确性与性能
-
值过小可能导致分析失败(如无法识别格式或漏掉某些流)。
-
值过大会增加启动延迟(尤其对网络资源)。
-
注意事项
-
优先级:
probesize
仅在初始阶段生效,不影响后续的实际解码或转码。 -
与格式探测的关系:
ffmpeg
可能需要在probesize
范围内找到有效的格式头(如moov
原子)。若失败,可尝试增大该值。 -
极端情况:设为
0
会让ffmpeg
使用默认值;设为极大值可能导致内存问题。
尝试解决
知道了probesize的含义,我们尝试把这个值设为一个较小的数字,让ffmpeg尽快去实际地处理h264数据。
ffmpeg_cmd = ['ffmpeg','-f', 'h264', "-probesize", "32", # 设置探测数据大小为 32 字节"-loglevel", "debug", # 启用调试日志'-i', "pipe:0" , "-s", "320x180", '-f', 'image2', os.path.join(IMAGE_DIR, "frame_%04d.png")
]
有了一点效果,但是问题还是存在,处理到22帧左右才开始输出图片:
为什么呢?
这就要从h264图像压缩技术说起了。
H.264 视频帧技术简介
H.264(也称为**AVC**,Advanced Video Coding)是一种广泛使用的视频压缩标准,能够以较低的码率提供高质量的流媒体和存储视频。它采用多种技术来减少视频数据的冗余性,从而提高压缩效率。
1. H.264 视频帧类型
H.264 将视频帧分为三种主要类型,以适应不同的压缩需求:
(1) I 帧(Intra Frame / Key Frame)
-
特点:
-
不依赖其他帧,独立压缩(类似JPEG)。
-
占用存储空间较大,但解码无需参考其他帧。
-
-
作用:
-
作为视频的 “关键帧”,用于随机访问(如视频跳转)。
-
通常在 GOP(Group of Pictures)序列的起始位置出现。
-
(2) P 帧(Predictive Frame)
-
特点:
-
依赖前一帧(I帧或P帧)进行压缩,存储**运动补偿**和**变化信息**。
-
比I帧占用更少的比特率。
-
-
作用:
-
通过运动估计(Motion Estimation)减少时间冗余。
-
提高压缩率,但仍解码较快。
-
(3) B 帧(Bi-directional Frame)
-
特点:
-
双向预测,依赖**前、后帧**(I或P帧)进行压缩。
-
压缩率最高,但解码复杂度更高(需缓存后向帧)。
-
-
作用:
-
进一步减少冗余,提高压缩效率。
-
常用于高质量编码(如蓝光电影)。
-
2. 典型H.264码流结构
-
NAL(Network Abstraction Layer):
-
H.264按**NAL单元(NALU)** 组织数据,方便网络传输。
-
包含**SPS(序列参数集)、PPS(图像参数集)、I/P/B帧数据**。
-
-
GOP(Group of Pictures):
-
一组连续帧(如
I B B P B B P B B I
)。 -
Closed GOP
(无跨GOP参考) vs.Open GOP
(允许B帧前向参考)。
-
而ffmpeg为了加快处理速度,会采用多线程的方式来解码h264视频帧, 默认会使用帧级多线程(Frame-Based Multi-Threading),通过 -threads
参数控制。
-
如果未显式设置
-threads
,默认线程数通常为逻辑CPU核心数(但可能受编解码器内部限制)。 -
可通过
-thread_type slice
或-thread_type frame
指定线程模式。
这样就会有问题,由于B帧/P帧的依赖关系, H.264 的帧间压缩(尤其是B帧)需要参考前后帧,多线程解码时可能导致线程阻塞等待依赖帧。例如:某线程解码一个B帧需要等待后续P帧完成,但后续帧由另一线程处理,此时会阻塞。
知道了问题原因,就有两种解决方案。
解决方案1: 减少线程数
ffmpeg_cmd = ['ffmpeg','-f', 'h264', "-probesize", "32", # 设置探测数据大小为 32 字节"-threads", "1", # 使用单线程"-loglevel", "debug", # 启用调试日志'-i', "pipe:0" , "-s", "320x180", '-f', 'image2', os.path.join(IMAGE_DIR, "frame_%04d.png")
]
解决方案2: 指定成切片级多线程
ffmpeg_cmd = ['ffmpeg','-f', 'h264', "-probesize", "32", # 设置探测数据大小为 32 字节"-thread_type", "slice", # 使用切片级多线程"-loglevel", "debug", # 启用调试日志'-i', "pipe:0" , "-s", "320x180", '-f', 'image2', os.path.join(IMAGE_DIR, "frame_%04d.png")
]
效果
可以看到,目前在第7帧左右开始输出实际的图像,为什么还是有7帧的延迟呢? 还是因为关键帧问题,这个没办法避免了。