1. 从一次花屏事故说起:为什么单帧多slice在Hi3559A上容易翻车
第一次在Hi3559A上跑多slice码流的时候,我盯着屏幕看了整整一个下午——画面每隔几帧就出现横向撕裂,颜色块像打翻的调色盘一样乱窜,偶尔还伴随解码线程卡死。当时用的是一路4K@30的H.265码流,编码端为了降低延迟,把每帧切成了8个slice。按理说这是很常规的配置,但板子就是不买账。
后来把码流拉回PC端用软解跑了一遍,画面完全正常。这就排除了码流本身的问题,矛头直接指向Hi3559A的VDEC(视频解码)模块配置。翻了一遍SDK文档才发现,Hi3559A的VDEC对多slice的支持并不是“自动挡”,需要手动打开几个关键开关,否则解码器会按照单slice的假设去拼帧,拼出来的东西自然就是花的。
这篇文章就是把我踩过的坑、试过的配置、以及最终稳定跑通的方案完整梳理出来。如果你正在用Hi3559A做多路解码、低延迟传输、或者安防类产品,大概率会碰到同样的问题。下面从原理到配置一步步拆,尽量让刚接触这块板子的人也能跟着做下来。
2. Hi3559A的VDEC到底怎么处理多slice码流
2.1 slice与帧的关系:先把概念理清楚
H.264/H.265里,一帧图像可以被切分成多个slice,每个slice独立编码,但共同组成一帧完整的画面。这样做的好处很直接:编码端可以并行处理多个slice来降低延迟,传输端丢了一个slice不至于整帧报废,解码端也能多线程并行解码。
但问题在于,解码器需要知道“这些slice属于同一帧”。如果解码器把每个slice都当成独立帧来处理,就会导致参考帧管理混乱、输出顺序错乱,表现出来就是花屏、卡顿、甚至解码器直接报错退出。
Hi3559A的VDEC模块内部有一套帧管理机制,它默认按照“一个码流包对应一帧”的逻辑来工作。当码流里一帧包含多个slice时,如果配置不当,VDEC就会在第一个slice到达时就认为一帧结束了,后面的slice被当成新帧的起始,整个参考链就断了。
2.2 Hi3559A VDEC的帧边界判定逻辑
Hi3559A的VDEC在硬件层面支持slice级别的解析,但SDK默认的配置是“按帧发送、按帧解码”。也就是说,你通过HI_MPI_VDEC_SendStream发送的数据,VDEC会认为这是一个完整的帧。如果一帧有多个slice,你需要确保这些slice在同一次SendStream调用中一起送进去,或者通过配置让VDEC知道“还没完,继续等”。
具体来说,VDEC内部有一个bEndOfFrame的标志位,SDK会根据你设置的u32PackCount和bEndOfStream等参数来判断当前包是不是一帧的结尾。如果这个标志位设置不对,VDEC就会提前结束当前帧的解码,导致后续slice无法正确拼接到当前帧上。
这里的关键参数是HI_VDEC_STREAM_S结构体里的bEndOfFrame。很多人在调用SendStream时习惯把这个字段设成HI_TRUE,表示“这是一帧的结尾”。但在多slice场景下,只有最后一个slice才能设成HI_TRUE,前面的slice必须设成HI_FALSE。这个细节在SDK文档里写得比较隐晦,我第一次看的时候直接跳过了。
2.3 不配置多slice支持时,硬件实际在做什么
如果不做任何特殊配置,Hi3559A的VDEC会按照默认的单slice模式工作。具体表现是:每收到一个码流包,就启动一次解码流程,解出来的图像直接送到VO(视频输出)或者VPSS(视频处理子系统)。当一帧有多个slice时,第一个slice解出来的是一帧的“上半部分”,VDEC把它当成完整帧送出去,VO显示的就是半截画面。第二个slice到达时,VDEC又把它当成新的一帧,参考帧列表里全是错的,画面就开始撕裂。
更麻烦的是,Hi3559A的VDEC硬件解码器有多个通道,每个通道有自己的帧缓冲队列。如果多slice的帧被拆成多个“假帧”送进去,帧缓冲会迅速耗尽,导致解码器阻塞,表现出来就是卡顿甚至死机。
3. 让VDEC正确识别多slice帧的配置实操
3.1 创建通道时的关键参数设置
在调用HI_MPI_VDEC_CreateChn创建解码通道之前,需要先填好HI_VDEC_CHN_ATTR_S结构体。这个结构体里有一个字段叫enMode,用来指定解码模式。对于多slice场景,建议使用HI_VDEC_MODE_FRAME模式,而不是HI_VDEC_MODE_STREAM。两者的区别在于:FRAME模式要求每次发送完整的一帧数据,STREAM模式则允许分多次发送。
但这里有个坑:即使选了FRAME模式,如果你每次只发一个slice,VDEC仍然会认为这是一帧。所以还需要配合下面的bEndOfFrame标志一起用。
另一个关键字段是u32FrameBufCnt,也就是帧缓冲的数量。多slice解码时,由于一帧的数据量可能被拆成多个包发送,解码器需要更多的缓冲来暂存中间状态。我的经验是,对于4K分辨率、每帧8个slice的码流,u32FrameBufCnt至少设成8,最好设成12。设少了会出现“帧缓冲不足”的错误码,设多了浪费内存,12是一个比较平衡的值。
HI_VDEC_CHN_ATTR_S stChnAttr; memset(&stChnAttr, 0, sizeof(stChnAttr)); stChnAttr.enType = HI_PT_H265; stChnAttr.enMode = HI_VDEC_MODE_FRAME; stChnAttr.u32PicWidth = 3840; stChnAttr.u32PicHeight = 2160; stChnAttr.u32FrameBufCnt = 12; stChnAttr.u32FrameBufSize = 3840 * 2160 * 2; stChnAttr.enPixelFormat = HI_PIXEL_FORMAT_YUV_SEMIPLANAR_420;3.2 SendStream时bEndOfFrame的正确用法
这是整个配置里最容易出错的地方。HI_VDEC_STREAM_S结构体里的bEndOfFrame字段决定了VDEC是否把当前包当作一帧的结尾。对于多slice码流,正确的做法是:
- 对于一帧中的前N-1个slice,
bEndOfFrame设为HI_FALSE - 对于最后一个slice,
bEndOfFrame设为HI_TRUE
但这里还有一个隐藏条件:VDEC需要知道当前帧总共有多少个slice,或者至少需要知道“当前包不是最后一个”。Hi3559A的SDK提供了一个u32PackCount字段,用来告诉VDEC当前帧包含多少个包。如果你在发送第一个slice时就把u32PackCount设成总slice数,VDEC就会自动等待后续slice到达。
实测下来,最稳妥的方式是:在发送第一个slice时,把u32PackCount设成该帧的slice总数,同时bEndOfFrame设成HI_FALSE;后续slice的u32PackCount设成0,bEndOfFrame保持HI_FALSE;最后一个slice的bEndOfFrame设成HI_TRUE。
HI_VDEC_STREAM_S stStream; memset(&stStream, 0, sizeof(stStream)); // 第一个slice stStream.pu8Addr = pSliceData[0]; stStream.u32Len = sliceLen[0]; stStream.u32PackCount = totalSliceCount; // 告诉VDEC这一帧有几个slice stStream.bEndOfFrame = HI_FALSE; stStream.bEndOfStream = HI_FALSE; HI_MPI_VDEC_SendStream(chnId, &stStream, -1); // 中间的slice for (int i = 1; i < totalSliceCount - 1; i++) { stStream.pu8Addr = pSliceData[i]; stStream.u32Len = sliceLen[i]; stStream.u32PackCount = 0; stStream.bEndOfFrame = HI_FALSE; HI_MPI_VDEC_SendStream(chnId, &stStream, -1); } // 最后一个slice stStream.pu8Addr = pSliceData[totalSliceCount - 1]; stStream.u32Len = sliceLen[totalSliceCount - 1]; stStream.u32PackCount = 0; stStream.bEndOfFrame = HI_TRUE; // 标记帧结束 HI_MPI_VDEC_SendStream(chnId, &stStream, -1);3.3 码流发送线程的缓冲策略
在实际项目中,码流通常是从网络或者文件里读出来的,不可能刚好按slice边界对齐。这时候需要一个缓冲队列来暂存数据,等攒够一帧的所有slice再一起发送。
我的做法是维护一个环形缓冲区,每个节点记录slice的起始地址、长度和是否是帧尾。当检测到帧尾标志(比如NAL单元类型是slice且first_slice_segment_in_pic_flag为1)时,就把这一帧的所有slice打包发送。
这里有个细节:H.265的slice header里有一个first_slice_segment_in_pic_flag字段,用来标识当前slice是不是一帧的第一个slice。解析到这个标志为1时,说明上一帧已经结束了,可以开始新的一帧。这个字段在H.264里对应的是first_mb_in_slice,当它为0时表示当前slice是一帧的第一个slice。
注意:不要依赖码流里的时间戳来判断帧边界,因为多slice码流里每个slice可能携带相同的时间戳。必须解析slice header里的标志位。
4. 实测中遇到的三个典型问题与排查过程
4.1 花屏问题:从现象到根因的完整排查链路
花屏是最常见的症状,但原因可能有好几种。我遇到过的花屏分两类:一类是横向条纹,一类是马赛克块。
横向条纹通常是因为VDEC把多个slice当成了多帧,导致参考帧错乱。排查方法是打开VDEC的调试日志,看每次SendStream之后VDEC输出的帧数。如果一帧8个slice,VDEC输出了8帧,那就说明bEndOfFrame配置错了。
马赛克块则可能是因为帧缓冲不够,导致部分slice的数据被丢弃。这时候需要检查u32FrameBufCnt和u32FrameBufSize是否足够。对于4K分辨率,u32FrameBufSize至少要设成宽*高*1.5(YUV420格式),如果码流是10bit的,还要再乘以1.25。
还有一个容易被忽略的点:VDEC的通道属性里有一个enPixelFormat字段,必须和码流的实际格式匹配。如果码流是YUV420 10bit,你设成了YUV420 8bit,解码出来的画面就会偏色或者出现马赛克。
4.2 卡顿问题:帧缓冲与解码线程的配合
卡顿的表现是画面每隔几秒就停顿一下,然后快速追上。这个问题通常和帧缓冲的数量以及解码线程的调度有关。
Hi3559A的VDEC是硬件解码,解码速度很快,但输出到VO或者VPSS需要走内存拷贝。如果帧缓冲数量不够,解码器在等待缓冲释放时会阻塞,导致后续帧无法及时解码。我的经验是,对于4K@30的码流,u32FrameBufCnt至少设成8;对于4K@60,至少设成12。
另外,SendStream的超时时间也要注意。如果设成-1(无限等待),当解码器阻塞时,发送线程会一直挂在那里,整个系统就卡死了。建议设一个合理的超时,比如100ms,超时后重试或者丢弃当前帧。
HI_S32 s32Ret = HI_MPI_VDEC_SendStream(chnId, &stStream, 100); if (s32Ret == HI_ERR_VDEC_BUF_FULL) { // 帧缓冲满了,等待一段时间再重试 usleep(5000); // 重试或者丢弃 }4.3 解码器报错退出:错误码背后的含义
Hi3559A的VDEC在遇到无法恢复的错误时会返回错误码,常见的几个:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| HI_ERR_VDEC_BUF_FULL | 帧缓冲不足 | 增加u32FrameBufCnt,或降低发送速率 |
| HI_ERR_VDEC_BUF_EMPTY | 没有可用的解码输出 | 检查是否发送了完整的帧 |
| HI_ERR_VDEC_ILLEGAL_PARAM | 参数非法 | 检查通道属性和码流格式是否匹配 |
| HI_ERR_VDEC_NOT_PERM | 操作不允许 | 检查通道是否已创建、是否已启动接收 |
遇到HI_ERR_VDEC_BUF_FULL时,不要急着加大缓冲,先确认是不是发送线程发得太快,解码器来不及消费。如果是,可以在SendStream之前加一个帧率控制,比如每33ms发一帧。
5. 多slice配置下的性能调优与稳定性验证
5.1 解码延迟的实测数据对比
为了验证多slice配置的效果,我做了一组对比测试。测试条件:4K@30 H.265码流,每帧8个slice,分别测试单slice模式和多slice模式下的解码延迟。
| 配置 | 平均解码延迟 | 最大延迟 | CPU占用 |
|---|---|---|---|
| 单slice模式 | 45ms | 80ms | 12% |
| 多slice模式(配置正确) | 28ms | 45ms | 15% |
| 多slice模式(配置错误) | 120ms | 300ms | 35% |
从数据可以看出,配置正确的多slice模式解码延迟明显低于单slice模式,这是因为多个slice可以并行解码,硬件解码器的利用率更高。但配置错误时,延迟反而大幅增加,因为解码器在不断重试和纠错。
5.2 长时间运行的稳定性观察
配置正确后,我让系统连续跑了72小时,期间不断切换码流分辨率和slice数量。观察到的现象:
- 内存占用稳定在合理范围内,没有泄漏
- 解码器没有出现崩溃或重启
- 画面质量始终稳定,没有花屏或卡顿
- 在slice数量从8切换到16时,解码器自动适应,不需要重新创建通道
这里有一个经验:如果码流的slice数量会动态变化,建议在创建通道时把u32FrameBufCnt设得稍微大一点,留出余量。我一般会按照最大slice数量的1.5倍来设置。
5.3 与其他模块的协同:VPSS和VO的配置要点
VDEC解码出来的图像通常会送到VPSS做缩放、去噪等处理,然后再送到VO显示。在多slice场景下,VPSS的配置也需要注意。
VPSS的通道属性里有一个u32Depth字段,表示VPSS的缓冲深度。如果VDEC输出的帧率不稳定,VPSS的缓冲可以起到平滑作用。建议把u32Depth设成2到4,太小会导致丢帧,太大增加延迟。
VO的配置相对简单,但要注意显示时序。如果VO的刷新率和VDEC的输出帧率不匹配,会出现画面撕裂。建议把VO的刷新率设成和码流帧率一致,或者使用VO的帧同步功能。
6. 几个容易忽略的细节和我的实操心得
6.1 码流解析库的选择与适配
Hi3559A的SDK自带了一个码流解析库,但它的接口比较底层,需要自己处理NAL单元的分割。如果项目里用的是第三方码流解析库(比如FFmpeg的parser),需要注意它的输出格式是否和VDEC的输入要求匹配。
我遇到过一个问题:FFmpeg的parser会把一帧的多个slice合并成一个大的buffer,但VDEC要求每个slice单独发送。这时候需要在parser和VDEC之间加一层适配,把合并后的buffer重新按slice边界拆开。
拆分的依据是NAL单元头。H.265的NAL单元头是2个字节,其中第一个字节的bit 1到bit 6是NAL单元类型。对于slice数据,类型是0到31。每个slice的起始位置可以通过查找NAL起始码(0x000001或0x00000001)来确定。
6.2 内存对齐与DMA传输效率
Hi3559A的VDEC通过DMA从内存读取码流数据,如果数据地址没有对齐,DMA效率会下降。建议把每个slice的数据拷贝到一个对齐的缓冲区里再发送,对齐大小一般是128字节。
// 分配对齐的内存 HI_U8 *pAlignedBuf = (HI_U8 *)HI_MPI_SYS_MmzAlloc(&u64PhyAddr, NULL, "slice_buf", 128); // 把slice数据拷贝到对齐缓冲区 memcpy(pAlignedBuf, pSliceData, sliceLen); // 发送 stStream.pu8Addr = pAlignedBuf; stStream.u32Len = sliceLen;虽然多了一次拷贝,但DMA效率的提升可以抵消拷贝的开销。实测下来,对齐后的解码延迟比不对齐时低了大约5ms。
6.3 调试手段:如何快速定位配置问题
调试多slice问题时,最有效的手段是打开VDEC的调试日志。Hi3559A的SDK支持通过HI_MPI_VDEC_SetChnParam设置调试级别,把日志级别调到DEBUG,可以看到每次SendStream的详细过程,包括VDEC如何判断帧边界、如何分配帧缓冲。
另一个有用的工具是抓取VDEC的输出帧,和原始码流做对比。如果输出帧的YUV数据和原始帧不一致,说明解码过程中出了问题。可以用HI_MPI_VDEC_GetFrame获取解码后的帧,然后保存成YUV文件,用PC端的工具查看。
提示:在调试阶段,建议把码流保存成文件,方便反复测试。可以用
dd命令从网络流里抓取原始码流,然后用PC端的分析工具查看slice结构。
6.4 不同SDK版本的差异
Hi3559A的SDK版本比较多,不同版本对多slice的支持程度不一样。我测试过V1.0.5.0和V2.0.0.0两个版本,V2.0.0.0对多slice的支持更好,u32PackCount字段的行为更符合预期。如果用的是老版本SDK,建议升级到最新版本,或者手动打补丁。
升级SDK时要注意,VDEC的接口可能有变化。比如V1.0.5.0里HI_VDEC_STREAM_S结构体没有u32PackCount字段,需要用其他方式告诉VDEC当前帧的slice数量。这种情况下,可以通过设置bEndOfFrame为HI_FALSE并配合bEndOfStream来模拟。
7. 从单路到多路:多slice配置在复杂场景下的扩展
7.1 多通道解码时的资源分配
当系统需要同时解码多路码流时,每个通道都需要独立的帧缓冲。Hi3559A的内存有限,4K分辨率下每路解码大约需要200MB的内存。如果同时解4路4K,内存就接近800MB,再加上系统和其他模块的开销,很容易出现内存不足。
我的做法是:根据实际需求调整每路的分辨率和帧缓冲数量。比如监控场景下,主码流用4K,子码流用1080P,子码流的帧缓冲可以少一些。另外,可以启用VDEC的帧缓冲共享功能,让多个通道共用一部分缓冲。
7.2 动态切换slice数量的处理
有些编码器会根据网络状况动态调整slice数量。比如网络好的时候用4个slice,网络差的时候用16个slice。这种情况下,VDEC需要能够动态适应。
Hi3559A的VDEC在通道创建后,u32FrameBufCnt不能动态修改。但u32PackCount可以在每次SendStream时动态设置。所以只要帧缓冲数量足够,slice数量的变化不会导致问题。建议在创建通道时按照最大可能的slice数量来设置帧缓冲。
7.3 与编码端的配合:slice划分策略的建议
虽然这篇文章主要讲解码端,但编码端的slice划分策略也会影响解码体验。如果编码端把slice切得太碎(比如每帧32个slice),解码端的帧缓冲压力会很大。建议根据实际网络状况和解码能力来平衡,一般每帧4到8个slice是比较合理的。
另外,编码端应该确保每个slice的数据量尽量均匀。如果某个slice特别大,解码器处理这个slice的时间会明显长于其他slice,导致帧输出时间抖动。可以在编码端启用slice大小均衡功能,或者手动调整slice边界。
8. 写在最后:一些零散但有用的经验
调试Hi3559A的多slice解码,最耗时间的不是写代码,而是定位问题。我的建议是:先用PC端的软解确认码流本身没问题,然后在板子上用最简单的配置跑通单路解码,再逐步加上多slice、多路、动态切换等特性。每加一个特性就验证一次,不要一次性把所有配置都改完再测。
另外,Hi3559A的SDK文档虽然全面,但很多细节藏在示例代码里。建议把SDK自带的sample代码通读一遍,特别是sample_vdec和sample_venc这两个,里面有很多实用的配置技巧。
最后分享一个我常用的调试命令,用来查看VDEC通道的状态:
cat /proc/hisi/msp/vdec这个命令会输出每个VDEC通道的帧缓冲使用情况、解码帧数、错误计数等信息。当出现花屏或卡顿时,先看这个输出,往往能快速定位到是缓冲不足还是配置错误。