当前位置: 首页 > news >正文

记一次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 在开始处理输入文件时,最多读取多少数据来检测文件的容器格式、流信息(如视频、音频、字幕等)和其他元数据。

作用

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

http://www.xdnf.cn/news/299827.html

相关文章:

  • 个人码支付免签系统三网免挂支付宝微信QQ钱包即时到账收款二维码聚合支付源码
  • 使用 OpenSSL 吊销 Kubernetes(k8s)的 kubeconfig 里的用户证书
  • uv全功能更新:统一管理Python项目、工具、脚本和环境的终极解决方案
  • 嵌入式学习--江协51单片机day1
  • GCC编译器安装详细说明(举例arm-2013q3)
  • pywinauto通过图片定位怎么更加精准的识别图片?
  • 抖音代播领航者——品融电商(PINKROON)的运营实力与服务解析
  • 使用 AddressSanitizer 检测堆越界错误
  • 【CPU占用率查看】
  • 创建简易个人关系图谱(Neo4j )
  • 【落羽的落羽 C++】list及其模拟实现
  • On the Biology of a Large Language Model——论文学习笔记——拒答和越狱
  • 华为私有协议Hybrid
  • 5月6日日记
  • QtGUI模块功能详细说明,图像处理(三)
  • 目标检测(Object Detection)研究方向常用数据集简单介绍
  • 【Bootstrap V4系列】学习入门教程之 组件-卡片(Card)高级用法
  • 数据初步了解
  • 论文速读:《CoM:从多模态人类视频中学习机器人操作,助力视觉语言模型推理与执行》
  • 电池热管理CFD解决方案,为新能源汽车筑安全防线
  • TikTok 矩阵账号运营实操细节:打造爆款矩阵
  • SpringBoot整合Kafka、Flink实现流式处理
  • 三种信号本振
  • Redis 7.0中5种新特性及实战应用
  • 【ArcGISPro】创建要素和刷新数据库后卡顿
  • 浔川AI 第二次内测报告
  • 数据可视化与分析
  • Flutter开发IOS蓝牙APP的大坑
  • 购物数据分析
  • 云境天合水陆安全漏电监测仪—迅速确定是否存在漏电现象