记一次ffmpeg延迟问题排查

#王者杯·14天创作挑战营·第1期#

背景

最近需要使用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
    # 过滤没消息发出的topic
    rlist, _, _ = 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 = None
    if 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 在开始处理输入文件时,最多读取多少数据来检测文件的容器格式、流信息(如视频、音频、字幕等)和其他元数据。

作用

  1. 加速分析过程

    • 通过限制初始读取的数据量,避免 ffmpeg 无谓地扫描整个大文件(尤其是网络流或大型文件),从而加快分析速度。

    • 例如,对于远程直播流,设置较小的 probesize 可以更快地进入实际处理阶段。

  2. 处理不完整的文件或特殊格式

    • 某些文件(如损坏的或未完全下载的媒体)可能包含无效的头部信息。调整 probesize 可以强制 ffmpeg 在更早或更晚的位置检测格式。

    • 对于某些非标准格式(如无明确头部信息的流),可能需要增加 probesize 以确保正确识别。

  3. 平衡准确性与性能

    • 值过小可能导致分析失败(如无法识别格式或漏掉某些流)。

    • 值过大会增加启动延迟(尤其对网络资源)。

注意事项

  • 优先级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帧的延迟呢? 还是因为关键帧问题,这个没办法避免了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

递归书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值