如果你在调 hi3516ev200 这颗芯片的 RTMP 推流,程序跑起来之后控制台里突然刷出这么一行:
[SAMPLE_COMM_VENC_GetVencStreamProc]-2024: get venc stream time out, exit thr我猜你第一反应是去查推流库、查网络、甚至怀疑 RTMP 服务器是不是有问题。说实话,我最初也这么干过,翻了半天一无所获。后来才明白,这个报错根本不是网络问题,问题出在视频编码链路:你的程序在向编码器要一帧 H264/H265 数据的时候,等了几百毫秒甚至一秒,编码器一帧都没吐出来。它其实是采集、前处理、编码整条链路断掉之后,在编码器取流环节暴露出的一个信号。
这篇文章会把链路拆开,告诉你哪些环节断了会导致这个提示,哪些坑是 RTMP 推流场景下特有的,并且给出一套可以直接照做的排查流程。准备用这颗芯片做 IPC、做嵌入式推流设备的开发者应该都会遇到类似问题,尤其是正在拿 sample_venc 改业务的朋友,这篇文章能帮你从“瞎试”变成“按图索骥”。
1. 这个报错到底在报什么
1.1 报错位置和它的词义拆解
先看日志来源。[SAMPLE_COMM_VENC_GetVencStreamProc]是 SDK 里 sample_comm 公共函数库中的一个函数,名字翻译过来就是“公共 VENC 获取码流线程处理函数”。这个函数做的事情非常基础:在一个循环里,反复调用 VENC 通道的取流接口,拿到一组编码后的码流包,然后交给上层逻辑做封装处理。
它的核心逻辑大致长这样:
while (pstParam->bThreadStart) { VENC_STREAM_S stStream; memset(&stStream, 0, sizeof(stStream)); s32Ret = HI_MPI_VENC_GetStream(vencChn, &stStream, 1000); if (s32Ret == HI_ERR_VENC_BUF_EMPTY) { printf("[SAMPLE_COMM_VENC_GetVencStreamProc]-%d: get venc stream time out, exit thr\n", __LINE__); break; } // 处理码流,比如送给 RTMP 封装 handle_stream(&stStream); // 用完必须释放,否则 buffer 会被占满 HI_MPI_VENC_ReleaseStream(vencChn, &stStream); }注意日志里的-2024不是错误码,是源文件里的行号,不同 SDK 版本行号会不一样。真正关键的是HI_ERR_VENC_BUF_EMPTY这个错误,意思是:你向编码器通道要码流,但编码器输出缓冲区是空的。在超时时间内(示例里是 1000ms),没有任何一帧编码数据被生产出来。
很多人一看到time out就以为是超时时间设短了,直接往大了改,比如改成 3000ms 甚至 5000ms。这个操作本身没有意义。25fps 的编码帧间隔只有 40ms,正常编码器 1000ms 内能出二十多帧,一帧都不出,说明不是“慢”,是“断”。
1.2 超时背后是一条闭合链路
要理解这个报错,必须把视频链路摆在桌面上。从 sensor 到 RTMP 推流,中间是一条单向流水线:
sensor -> VI/ISP -> VPSS -> VENC -> 用户取流线程 -> RTMP 封装发送VI 负责把 sensor 的数据变成图像帧;VPSS 负责缩放、裁剪、降噪这类预处理;VENC 负责把图像编成 H264/H265 码流;最后取流线程从 VENC 通道里把码流包拿走,拼成 FLV 推出去。
这条链路上任何一个环节断了,VENC 都生产不出数据,最终都会在GetStream那个位置爆出time out。所以这个日志是一个典型的“下游报错、上游生病”的案例。这里可以用一个生活类比:你在食堂取餐窗口等饭,窗口里一直没人递饭出来,问题未必是窗口师傅偷懒,很可能是后厨压根没开火、菜没送过来、甚至师傅还没到岗。你要做的是去后厨查,而不是盯着窗口催。
弄清楚了这一点,排查思路就清晰了:从 VI 开始,一层一层往下查,看数据到底断在哪。
2. 逐个排查:哪一个环节没有出数据
2.1 先确认 VI 是真的出图了
所有问题的第一站就是 VI。如果 sensor 都没出图,后面全是空谈。很多情况下,sensor 初始化返回 0 不代表真的在出图,驱动加载成功和 ISP 正常出帧是两回事。
我建议先做一个最简单的验证:启动 VI 之后,调用一次取帧接口,看能不能从 VI 通道拿到一帧原始图像。
VIDEO_FRAME_INFO_S stFrameInfo; s32Ret = HI_MPI_VI_GetChnFrame(0, 0, &stFrameInfo, 1000); if (s32Ret == HI_SUCCESS) { printf("VI chn get frame ok, w=%u, h=%u\n", stFrameInfo.stVFrame.u32Width, stFrameInfo.stVFrame.u32Height); // 注意用完必须释放,否则 VI 内部缓冲会被耗尽 HI_MPI_VI_ReleaseChnFrame(0, 0, &stFrameInfo); } else { printf("VI chn get frame failed: 0x%x\n", s32Ret); }这一步能在最上游验证数据有没有产生。如果连这一步都失败,重点检查:
- sensor 驱动模块有没有加载成功,加载顺序对不对;
- sensor 的类型、分辨率、I2C 地址是否匹配实际硬件;
- ISP 是否完成了初始化,有没有给它足够的启动时间;
- VI 通道是否执行了
HI_MPI_VI_EnableChn。
我踩过的一个经典坑是:某款 sensor 在初始化之后需要额外延时才能稳定输出,我把延时去掉之后,ISP 没起来,VI 自然一帧不出,VENC 必然超时。后来在 VI 启动后加了 200ms 延时,问题立刻消失。很多“玄学超时”其实都跟初始化时序有关。
2.2 VPSS 通道绑定,问题高发区
如果你确认 VI 已经能出图,下一个要查的是 VPSS。VPSS 负责把 VI 送进来的图像做大分辨率转换和预处理,编码器通常不直接绑 VI,而是绑 VPSS 的某一个输出通道。原因很简单:sensor 输出的分辨率可能很大、格式也不是编码器直接想要的,中间得靠 VPSS 转一手。
这里最容易犯的错有三个:第一,只启动了 VI,没启动 VPSS;第二,VPSS Group 创建了但忘了StartGrp,或者对应的输出通道没EnableChn;第三,HI_MPI_SYS_Bind绑定关系填错了,把源和目标搞反,或者通道号对不上。
绑定关系长这样:
MPP_CHN_S stSrcChn, stDestChn; memset(&stSrcChn, 0, sizeof(stSrcChn)); memset(&stDestChn, 0, sizeof(stDestChn)); stSrcChn.enModId = HI_ID_VPSS; stSrcChn.s32DevId = 0; stSrcChn.s32ChnId = 0; stDestChn.enModId = HI_ID_VENC; stDestChn.s32DevId = 0; stDestChn.s32ChnId = 0; s32Ret = HI_MPI_SYS_Bind(&stSrcChn, &stDestChn); if (s32Ret != HI_SUCCESS) { printf("bind vpss->venc failed: 0x%x\n", s32Ret); }绑定之前要保证 VPSS 通道已经使能、VENC 通道已经创建,否则会返回失败。还有一个容易忽略的点:VPSS 输出通道的分辨率要和 VENC 编码输入分辨率匹配,或者由 VENC 端做裁剪,如果两边宽高不一致,编码器也可能一直吃不到有效帧。检查的时候不要只看SYS_Bind返回成功就放心,要用cat /proc/umap/vpss看一眼通道状态。
2.3 VENC 通道自己没有开始接收帧
VI 和 VPSS 都正常时可别高兴太早,问题也可能出在 VENC 自己身上。VENC 通道创建成功不代表它开始工作了,部分 SDK 里创建通道之后还必须调用HI_MPI_VENC_StartRecvFrame,编码器才会真正开始接收输入图像。如果漏了这一步,VENC 通道就是挂着的空壳,取流必然超时。
另外,检查VENC_ATTR_S里的配置:
- 编码协议必须是
PT_H264或PT_H265,千万别拿默认值直接跑; u32FrameRate不能设成 0,否则编码器不知道按什么节奏出帧;- 分辨率、宽高要和 VPSS 输出对齐,且最好满足对齐要求;
- 码率控制模式建议先用 CBR 固定码率,排查问题时变量更少。
VENC 的启动顺序一般是这样:
HI_MPI_VENC_CreateChn(0, &stVencAttr); HI_MPI_VENC_StartRecvFrame(0, &stRecvParam);很多人只记得CreateChn忘了StartRecvFrame,尤其从老 SDK 迁移过来的时候容易踩。如果代码里确实调用了StartRecvFrame,就用状态查询接口确认一下编码器是否真的在收帧,这也是我在后面要重点说的方法。
2.4 取流超时时间不是越大越好
回到超时时间本身。官方示例里GetStream的超时参数通常给 1000ms,这个值是合理的。我见过有人改成 100ms,那当然容易超时,因为帧率 25fps 时帧间隔都 40ms,100ms 只能等两三帧,稍微抖一下就超时。但如果你已经是 1000ms 还超时,那就别再加时间了。
计算依据很简单:超时时间至少要覆盖 3 到 5 个帧间隔。帧间隔等于 1000 除以帧率:
- 25fps,帧间隔 40ms,1000ms 可以等 25 帧;
- 10fps,帧间隔 100ms,1000ms 可以等 10 帧;
- 5fps,帧间隔 200ms,1000ms 也可以等 5 帧。
所以只要帧率不是低得离谱,1000ms 在理论上绝对够用。如果这个时长内一帧都拿不到,说明编码器压根没在工作,或者上游根本没把帧送进来。把超时加到 3000ms、5000ms,只会让程序在出错时卡得更久,页面看起来像死机一样,反而更难看。
不过有一种例外:如果只是启动阶段初始化慢,比如 ISP tuning 还没完成,卡住一瞬间后才会出帧。这种情况可以通过延迟启动取流线程来规避,后面排查章节会详细说。
3. 叠加 RTMP 推流后,这几个坑别忽略
3.1 发送线程阻塞导致 VENC buffer 占满
很多人的第一版代码长这样:一个线程里既调GetStream取码流,又调 RTMP 库把数据发出去。这个写法看着省事,但非常危险。RTMP 底层是 TCP,网络一抖动,socket 写缓冲可能瞬间塞满,RTMP_SendPacket会阻塞在那里几百毫秒甚至更久。这段阻塞时间内,取流循环停摆,没有调用ReleaseStream把 VENC 的 buffer 还回去。
VENC 内部的输出缓冲是有限的。一直不释放,缓冲池就会被占满。缓冲池满后,编码器新编码出来的帧可能无处安放,某些 SDK 版本甚至会直接丢弃新帧,更严重的会出现内部状态异常,导致后续GetStream一直拿不到数据,最终表现为time out。
解决思路很简单:把取流和发送拆成两个线程,中间用一个队列缓冲。取流线程只管GetStream后立刻放入队列、立刻ReleaseStream;网络线程只管从队列里拿数据发送。这样即使网络卡住,顶多是队列积压,不会堵住编码器取流的节奏。如果网络长时间卡死,还可以选择丢帧策略,优先丢掉非关键帧,保住画面的实时性。
3.2 裸流里的 SPS/PPS 别漏掉
这个问题虽然不直接导致time out,但排查报错的时候容易让人分心。VENC 输出的裸流里,IDR 帧前通常会带上 SPS/PPS 信息,但它们是独立的数据包,不是直接拼在 IDR 帧头里的。做 RTMP 封装时,你要从取到的VENC_PACK_S里根据数据类型把 SPS、PPS 找出来,单独组成 FLV 的 sequence header 发送。
如果这里处理不对,播放器会黑屏或者花屏,这时候你会误以为推流出问题了,往回查网络、查服务器,结果什么都查不出来。所以我建议排查time out的时候,先把 RTMP 封装逻辑简化:取到码流先写本地文件,用播放器确认本地文件能正常播放,再把封装逻辑加回来。这样能把“视频链路断没断”和“封装对不对”两个问题彻底分开。
3.3 内存与 MMZ 分配,低端芯片的隐形杀手
hi3516ev200 这类低成本芯片的内存本来就紧张,MMZ(多媒体内存)更是要精打细算。如果你把编码分辨率开得过高、码率开得过大,或者同时开了多路,MMZ 分配不到位,VENC 通道创建时可能部分 buffer 申请失败,但初始化流程未必会给你打印明显的错误日志,最后就会变成“莫名其妙的取流超时”。
排查方法很直接,跑起来后执行:
cat /proc/media-mem cat /proc/umap/venc看 VENC 通道的 buffer 状态。如果发现某些通道的 buffer 数量是 0,或者根本没有 VENC buffer 的日志段,那就是内存分配出了问题。可以先把分辨率降到 720p、或者把输出帧率降到 15fps 验证一下,如果不再超时,基本可以断定是资源不足。
调整方法一般是修改系统启动时加载芯片库的脚本里的 MMZ 配置,给 VENC 预留更合理的 buffer 大小。这里提醒一句:MMZ 不是越大越好,系统运行时还需要内存做应用和数据缓冲,留出余地很重要,具体大小得根据实际场景反复压测。
3.4 停止推流时别乱关通道
最后一个 RTMP 场景特有的坑,就是程序退出时的线程销毁顺序。我见过有人为了“干净退出”,在推流线程还在跑的时候直接调用HI_MPI_VENC_DestroyChn,结果取流线程瞬间返回错误,打印出来的恰好就是这个time out,然后线程异常退出。实际上这不是故障,是销毁顺序不对造成的“假超时”。
正确的退出顺序应该是:先把取流线程停下来,再执行HI_MPI_VENC_StopRecvFrame停止接收帧,接着做HI_MPI_SYS_Unbind解绑,最后才销毁 VENC 通道。如果是多线程结构,还要先把网络发送线程停掉,再停取流线程,避免队列还有数据没发完就释放了一堆资源。
4. 一套可以直接照做的定位流程
4.1 先把 RTMP 摘掉,验证编码本身
遇到这个报错,最高效的第一步不是去打日志,而是做“减法实验”。把 RTMP 相关代码全部注释掉,只保留 VI + VPSS + VENC 的编码链路,然后直接跑官方提供的 sample_venc 示例程序。如果 sample_venc 能正常输出 H264 码流文件,说明底层编码链路是通的,问题出在你的推流线程、队列、封装逻辑上。如果 sample_venc 也超时,那问题就在 VI、VPSS、VENC 这三个底层模块里,跟 RTMP 一点关系都没有。
这一步信息量非常大,直接帮你划定了排查范围,省掉一半的无头苍蝇时间。我在实际项目里几乎没有例外:只要用户说推流超时,我第一件事都是让他跑 sample_venc 试试。
4.2 状态打印能省一半时间
海思 SDK 提供了状态查询接口,这是定位编码问题最好的武器。在启动 VENC 之后,单独开一个地方每隔 500ms 查询一次:
VENC_CHN_STATUS_S stStatus; memset(&stStatus, 0, sizeof(stStatus)); HI_MPI_VENC_QueryStatus(0, &stStatus); printf("uCurFrames=%u, uChnBufCnt=%u\n", stStatus.uCurFrames, stStatus.uChnBufCnt);uCurFrames表示当前编码器已经编码输出的帧数。如果这个值一直为 0,说明编码器根本没有收到帧,问题在 VI 或 VPSS;如果它在持续增长,说明编码器工作正常,问题在取流调用方式或超时参数上。
同样可以查 VPSS 的状态,比如通过/proc/umap/vpss看各个通道有没有数据。我习惯把这几个状态汇总打印到同一个日志里,形成一个“链路仪表盘”。这样报错一出现,我马上就能从最近的日志里看到是哪一段先没了数据。
4.3 善用错误码和日志时间戳
GetStream返回的错误码值得单独打印一下,不要只盯着time out。不同错误码代表的状态不一样。拿到返回值之后,用0x%x格式打印出来,再去查 SDK 的 errno 定义,看看是 buffer 空、参数错误还是通道状态不对。有些情况下错误码会明确告诉你通道没有使能,这时候就根本不用纠结超时。
另外,日志一定要带相对时间戳。我在现场排查时养成了一个习惯:所有关键动作打印时间,精确到毫秒。这样你能看出 sensor 初始化花了多久、VENC 创建花了多久、从启动到第一次取流超时经过了多久。如果每次都是初始化后 500ms 左右超时,大概率是时序问题;如果是运行很久之后才超时,大概率是资源泄漏或 buffer 堆积。
4.4 从后往前分段放宽,快速锁定
还有一个很实用的对照实验法。保持超时时间 1000ms 不变,在启动取流线程前加一个usleep(1000 * 1000),也就是等 1 秒再开始取流。如果加了延时之后报错消失,说明链路本身没问题,只是启动阶段 VI/sensor 还没就绪,取流线程开太早了。这是最常见的“假超时”。
如果延时后问题依旧,就把超时时间从 1000ms 临时改成 3000ms,观察报错是否出现。如果改成 3000ms 后仍然超时,说明链路完全中断;如果不再报错,说明链路“很慢但不完全断”,这时候继续往上查 VI/VPSS 的帧率。这个方法比盲目打日志更快,因为对照实验一次就能排除一半的可能原因。
| 排查模块 | 常用检查点 | 常见异常表现 |
|---|---|---|
| VI | VI_GetChnFrame能否拿到帧 | sensor 无输出、ISP 未起、通道未使能 |
| VPSS | Group/Chn 是否 Enable、SYS_Bind返回 | 通道未启动、绑定关系错误、分辨率不匹配 |
| VENC | StartRecvFrame是否调用、QueryStatus | 参数不对、未开始接收、MMZ 不足 |
| 取流线程 | GetStream返回值、超时时间 | 超时过短、发送阻塞、线程退出顺序错误 |
5. 能直接参考的代码与配置片段
5.1 正确的绑定与启动顺序
把几个关键动作按顺序列一下:
- 初始化 VI,确认能拿到帧;
- 初始化 VPSS Group 和输出通道,
EnableChn; - 创建 VENC 通道,设置编码属性;
HI_MPI_SYS_Bind绑定 VPSS 通道到 VENC 通道;HI_MPI_VENC_StartRecvFrame开始接收帧;- 延时 500ms 左右,再启动取流线程。
绑定关系的参考代码我在 2.2 节已经给过了,这里补充一个可能被忽略的点:HI_MPI_SYS_Bind绑定之后不要马上启取流线程。虽然示例里 SYS_Bind 是同步返回的,但实际硬件上数据通路建立是需要一点时间的,尤其 ISP 还在做自动曝光收敛的时候。给个 300 到 500ms 的延时,能避免很多启动瞬间的超时问题。
5.2 取流循环参考
取流线程里最关键的两个动作是GetStream和ReleaseStream必须成对出现。每次都不要忘记释放:
while (bThreadRunning) { VENC_STREAM_S stStream; memset(&stStream, 0, sizeof(stStream)); s32Ret = HI_MPI_VENC_GetStream(0, &stStream, 1000); if (s32Ret != HI_SUCCESS) { // 这里不要立刻 break,可以记录计数并continue continue; } for (u32 i = 0; i < stStream.u32PackCount; i++) { VENC_PACK_S *pstPack = &stStream.pstPack[i]; // pstPack->DataType 区分 SPS/PPS/IDR/P // pstPack->pAddr + pstPack->u32Len 是码流数据 } HI_MPI_VENC_ReleaseStream(0, &stStream); }注意一点:GetStream返回错误时,不要直接break退出线程,否则一次小抖动就把整个推流干掉。更稳妥的做法是记录连续失败次数,超过比如 20 次再退出,同时打印现场状态。这样程序有一定的容错能力,排查问题时日志也更有价值。
5.3 启动参数、MMZ 和线程设计建议
编码参数方面,第一版跑通建议用最稳的组合:H264、CBR 码率控制、GOP 设成帧率的两倍左右、分辨率匹配实际 sensor 输出。等整条链路稳定了,再逐步调码率、帧率和编码质量,避免一上来一堆变量互相干扰。
内存方面,先跑cat /proc/media-mem看当前占用,再决定是否调整加载脚本里的 MMZ 参数。如果 VENC buffer 明显不够,在加载脚本里为 VENC 模块多分配一些 buffer;如果只是单路 1080p,默认配置很多时候够用,先不要动,动了反而容易引入新的不稳定因素。
线程设计方面,建议取流线程和网络发送线程分离,队列长度按“网络抖动 1 秒”的量级来设计。比如 25fps、每帧平均 20KB 码流,1 秒大约 500KB,队列开 1MB 左右就能容忍 2 秒的网络卡顿;超过之后直接丢帧,确保画面延迟不会越来越大。丢帧策略上优先丢 P 帧,保留下一个 IDR 帧到来之前的画面连贯性。
最后说点个人的真实体会。遇到这个报错时,最容易做的事情就是延长超时时间,我刚开始也这么干,结果偶尔能跑、偶尔又崩,反而把问题掩盖了。后来我把每次出现这个日志时的QueryStatus数据都记下来,才发现大部分时候其实是编码器一开始就没吃进帧,超时只是最后的结果。调这种底层链路,说到底就是拆。把 VI、VPSS、VENC、推流四段拆开测,哪一段断了补哪一段,而不是在一整条链路上瞎猜。希望这篇内容能帮你尽早定位问题,少熬几个夜。