本文系微信公众号和知乎专栏《MediaStack》原创文章,欢迎大家关注,随时进行交流。

背景介绍
本周收到这样一个反馈的问题:我们的测试小姐姐通过自研RTSP客户端拉流过程时发现了一个较为困扰的问题。她注意到,在操作一款特定型号的IPC设备时,每次拉流都会出现首帧绿屏的情况。这个异常状况出现的频率和持续性都让人无法忽视,它在很大程度上影响了使用者的体验。
业务研发的同事们在面对这个问题时,暂时还没有找到具体的解决方案。尽管他们已经尝试了几种可能的编码或设备设置调整,但是目前这个首帧绿屏的问题依旧存在。因此,他们需要获取一些外部的专业协助,以便尽快找到问题的根源并有效地解决它。

问题分析
前面就是我们接手的任务:我们需要深入研究这款IPC设备在进行RTSP拉流时出现首帧绿屏的问题,然后制定出相应的解决策略,保证用户的体验。
首先,我们从硬件和软件两方面进行详细的分析:
在硬件层面,我们将查看设备的规格和配置是否有可能导致这个问题,确定了不同产品系列哪些会产生,而哪些又不会产生,经过交叉验证(不同IPC,不同设备之间相互测试),发现只有特定的一款机型+该款IPC会复现问题。那基本排查设备硬件问题。
在软件层面,我们详细交叉验证自研RTSP客户端,VLC客户端,以及其他RTSP客户端和该IPC的拉流对比,并进行网络协议以及RTSP拉流的参数相关设置完成验证,确定该IPC用Android自带的硬件解码时才会复现,而用软解则是正常的。
目前可以大概确定:该问题是软件问题,并且和码流有相当大的关系。
接下来,我们将对是否应修改编码格式或是进行其他可能的设备设置调整逐步排查,经过不断分析我们找到造成这个首帧绿屏问题的原因:第一个IDR Slice头中frame_num值为非零值。
那找到这个问题之后,就将其做一下优化,并反馈给对应的硬件厂商完成兼容调整。
同时需要说明的是SPS中log2_max_frame_num_minus4值又是零值:

标准说明
在H264的标准中,slice头的语法如下:
frame_num的值约束如下:
– 如果当前图像是IDR图像,frame_num应等于0。
– 否则(当前图像不是IDR图像),参考解码顺序中包含参考图像的前一个访问单元中的主编码图像作为前一个参考图像,当前图像的frame_num值不应相等 除非满足以下三个条件,否则为 PrevRefFrameNum:
a)当前图像和前一参考图像按照解码顺序属于连续的访问单元。
b)当前图像和前一参考图像是具有相反奇偶性的参考字段。
c) 满足以下一项或多项条件:
– 前面的参考图像是IDR图像,
– 前面的参考图像包括等于 5 的 memory_management_control_operation 语法元素,
注 3 – 当前面的参考图像包括等于 5 的 memory_management_control_operation 语法元素时,PrevRefFrameNum 等于 0。
– 存在在前一个参考图像之前的主编码图像,并且在前一个参考图像之前的主编码图像的frame_num不等于PrevRefFrameNum,
– 存在在前一个参考图像之前的主编码图像,并且在前一个参考图像之前的主编码图像不是参考图像。
这段明确表明,如果该帧图像是IDR图像,则frame_num是0值。然而我们碰到的问题IDR值非零值,所以导致解码异常了。

FFMPEG
我们进行过交叉验证,软解不会,那我们借用FFMPEG代码确认一下是否如此呢。
H264 slice解析过程如下,就不再完整解读了,我们挑选其中关键步骤说明说明该问题。
Slice定义
解析获取
h264_slice_header_parse
h264_slice_header_parse 解析 H.264 切片的标头并填充相应的 H264SliceContext 结构,该函数的具体解析过程如下图所示:
其中和本文有关系的部分如下代码,解析获取frame_num相关值:
对于帧图像,则curr_pic_num直接赋值为frame_num。这是因为每个帧都有唯一的帧号。
max_pic_num 计算为 2 的 sps->log2_max_frame_num 次方。该公式确保最大图像数量可以容纳序列中的所有潜在帧。
而对于场图像,curr_pic_num 计算为frame_num 的2 倍加1。这是因为场图像是隔行扫描的,这意味着两个场图像一起代表一帧。加 1 确保奇数编号的图像为顶场,偶数编号的图像为底场。
场图像的 max_pic_num 是通过将 2 的 sps->log2_max_frame_num + 1 次方计算出来的。这个附加位说明了顶部和底部场的单独编号方案。
之后h264_slice_header_parse会调用ff_h264_parse_ref_count进行参考帧的获取:
ff_h264_parse_ref_count会判断帧类型,如果是非I帧才会进行参考帧数量计算,而I帧则直接置零:
这段代码有以下几个作用:
检查当前帧编号 (h->poc.frame_num) 是否与前一帧编号 (h->poc.prev_frame_num) 不同。
(1)如果存在差异,会尝试通过以 SPS(序列参数集)中定义的log2_max_frame_num帧标志号消除这段差异;
(2)将当前帧编号和解包的前一帧编号 (h->poc.frame_num - unwrap_prev_frame_num) 之间的差异与指定的 sps->ref_frame_count 进行比较。如果差异超过参考帧计数,则需要超过分配的参考帧缓冲区,将解包的前一帧编号 (unwrap_prev_frame_num) 调整到参考帧限制内。
(3)采用边界检查来确保调整后的前一帧编号 (unwrap_prev_frame_num) 保持在有效范围内。
至此poc的frame_num也用slice的frame_num完成赋值,同时ref_count值也完成赋值,为0个;
虽然这些地方可以正确获取frame_num值,但是在decode_nal_units解码时,如果当前帧是IDR帧,则会进行一次刷新操作,之后所有的参考帧系列都置空了,也就不会受到Frame_num值的影响了。
IDR函数处理 H.264 解码过程中的瞬时解码器刷新 (IDR) 事件:调用 ff_h264_remove_all_refs 删除对先前解码图片的所有引用,确保解码后续 IDR 图片和任何相关图片的干净状态。
ff_h264_remove_all_refs,删除 H264Context 结构中对先前解码的 H.264 图片的所有引用,包括参考帧的所有相关信息,确保正确的资源管理并维护解码器的内部状态。
所以FFMPEG中对于IDR中slice frame_num值是否置位并无太大影响。


参考文献
https://www.cnblogs.com/ranson7zop/p/7603836.html
https://www.ramugedia.com/frame-num-is-not-frame-counter
http://www.staroceans.org/e-book/vcodex/H264_picmanagement_wp.pdf
https://blog.csdn.net/yu_yuan_1314/article/details/9011899

我是一枚爱跑步的程序猿,维护公众号和知乎专栏《MediaStack》,有兴趣可以关注,一起学习音视频知识,时不时分享实战经验。
继续阅读
阅读原文