做视频中间件这行的人,多半都遇到过同一个场景:现场装了一批海康的消费级或工程级摄像机,走的是 E-home(也就是常说的 ISUP/EHome 私有协议)注册到平台,设备端只填了个服务器地址和端口,剩下的什么都没给你。这时候前端同事跑过来问,能不能给个 RTSP 地址让 VLC 先播一下,或者能不能在浏览器里直接看。你打开设备 Web 一看,压根没有 RTSP 那一栏,抓包全是看不懂的私有报文。
E-home 私有协议接入并输出标准 FLV/HLS/RTSP 流,本质上就是解决这件事:把只有平台能懂的私有码流,翻译成所有播放器、浏览器、NVR 都认识的标准流。这篇文章不讲概念,讲的是我实际落地过的一套中间件架构——设备怎么注册上来、回调里吐出来的到底是什么数据、怎么剥成裸的 H.264/H.265、三条输出链路各自该怎么配、以及从"能播"到"跑一个月不掉线"之间那段最难走的路。
适合谁看:正在做安防平台对接的研发、需要把海康设备接进自己业务系统的集成商、以及被"设备在线但取不到流"折磨过的运维。前提是你对 TCP、H.264 的 NALU 有个基本概念,没有也没关系,关键的地方我会用生活化的比喻补上。
1. 先想清楚:私有协议为什么必须中间件兜一层
1.1 E-home 设备和标准播放器之间隔着三件事
很多人第一反应是"设备不是有 IP 吗,直接连上去取流不就行了"。这个思路在 ONVIF 设备上成立,在 E-home 设备上直接死掉,原因有三层。
第一层是连接方向反了。E-home 设备的典型工作模式是设备主动去连平台,而不是平台去连设备。设备端配置里填的是"服务器地址 + 端口 + 设备 ID",上电后它自己发起 TCP 连接注册上来,然后保持长连接做心跳。这意味着设备很可能在 NAT 后面、在 4G 网络里、在一个你根本 ping 不通的网段,你想从平台侧主动连它,门都没有。中间件必须提供一个公网可达的监听面,等着设备来注册。
第二层是信令和媒体是私有的。注册、心跳、报警、云台控制、取流请求,全都跑在厂商自己定义的一套报文里。你要取一路实时流,得先按它的规则发一个取流请求,然后它才在已建立的连接上往你这里推数据。这一整套没有公开标准,只能靠厂商提供的 SDK 或者自己逆出来,前者是正路。
第三层是推过来的媒体数据也不是标准容器。回调给你的通常是一段带私有头的数据包,私有头后面跟着 PS 封装(Program Stream),PS 里面才是真正的 H.264/H.265 码流。PS 这个格式播放器基本不认,你需要把它拆开,重新封成 FLV、TS 或者 RTP。
把这三层想通了,中间件的定位就清晰了:它是一台双向翻译机,对外用私有协议跟设备说话,对内用标准协议跟播放器说话。
1.2 设备主动注册,决定了中间件必须有公网可达的监听面
这一条看起来是废话,实际上是设计里最容易出问题的约束。因为连接是设备发起的,所以:
- 中间件必须监听一个固定端口,这个端口要写在设备配置里,不能随便变;
- 端口必须公网可达或者至少设备所在网络可达,涉及防火墙、安全组、端口映射;
- 设备会带着一个全局唯一的设备 ID注册上来,中间件要有一张映射表把这个 ID 对应到具体的设备记录、通道、以及后续的流地址;
- 连接是长连接,设备掉线不会通知你,只能靠心跳超时判断。
常见部署里,注册监听端口用 7660,媒体端口另配或者复用同一个端口,具体取值必须和设备端配置、以及你手上 SDK 的文档保持一致——不同版本、不同固件对这两个端口的处理不完全一样。我的建议是:中间件把端口做成配置项,并且启动时把实际监听的端口和协议模式打进日志。现场排查的第一件事永远是"设备填的端口和中间件听的端口是不是同一个",我见过太多因为一个数字对不上排查了半天的案例。
还有一个坑:如果你有多台中间件做集群,设备注册到哪台是不确定的。要么在设备侧按 ID 段分流,要么用一台前置做注册路由,把设备连接转发到后端节点。第二种复杂度高,小规模部署不要碰。
1.3 三种接入路线的账要提前算
落地前一定要把架构选型定下来,中途换路线等于重做。我把实际见过的三种路线列一下:
| 路线 | 做法 | 优点 | 代价 | 适合规模 |
|---|---|---|---|---|
| 全自研 | SDK 回调 → 自己解 PS → 自己写 RTP/FLV/TS 封装器 | 资源占用最低,全链路可控,延迟可压到很低 | 工作量大,要对 MPEG-PS、RTP、TS 都很熟,坑多 | 50 路以上、有专职流媒体同学 |
| 半自研 | SDK 回调 → 交给 FFmpeg(进程或库)转封装 → 推给流媒体服务器分发 | 开发快,兼容性好,格式问题交给 FFmpeg | 每路一个进程时资源偏重,管道容易堵 | 几路到几十路,主力方案 |
| 一体化 | 直接把接入逻辑塞进 ZLMediaKit / SRS 这类流媒体服务器 | 部署简单,一套软件搞定 | 定制能力受限,私有协议部分还是要自己写 | 小规模、快速验证 |
我最后选的是半自研:中间件负责私有协议接入和转封装,转出来的流推给流媒体服务器,由它统一对外提供 RTSP、HTTP-FLV、HLS 三种出口。理由很直白——分发这件事流媒体服务器已经做得很好了,自己再写一遍纯属浪费,而私有协议接入这块没人能替你做。分层的边界放在"转封装完成之后",往上都是标准协议。
2. 设备端配置与注册链路:接入能不能成,八成看这一步
2.1 设备侧只需要填四样东西
别把设备端配置想复杂了。E-home 接入的设备侧,核心就四个字段:
- 平台接入方式:选 E-home 或 ISUP,有些固件里叫"主动注册";
- 服务器地址:中间件所在机器的公网 IP 或者域名,建议用域名,后面换机器不用挨个改设备;
- 端口:和中间件监听的注册端口一致;
- 设备 ID:默认一般是设备序列号,也可以自定义,但必须全局唯一且中间件能识别。
这里有两个实操细节值得单独说。
第一个是设备 ID 不要用序列号直接当业务主键。序列号是可变的(主板返修换过、固件刷过都可能变),而且现场经常出现设备换了但人没告诉你。我的做法是中间件收到注册后,先按设备 ID 查映射表,查到了就用绑定的业务设备记录,查不到就落一条待绑定记录,在管理后台里人工确认。这样设备 ID 变化不会把业务数据搞乱。
第二个是版本选择要看设备支持哪些。EHome 有 2.0 和 5.0(也就是 ISUP 5.0)两代,老设备可能只支持 2.0,新设备两种都支持。两代协议在注册报文、心跳间隔、媒体封装上都有差异,中间件通常需要同时支持。判断方法很简单:把设备端两种模式各切一次,看平台侧收到的是什么,日志里能看出明显差别。不要相信设备铭牌,实测为准。
2.2 中间件监听服务的初始化与回调分派
以常见的 ECMS 系列接口为例(不同 SDK 版本函数名会有差异,比如取流接口就有NET_ECMS_StartGetRealStream和带版本后缀的变体,一切以你手上头文件为准),初始化流程大致是这样:
// 1. 全局初始化,进程启动时调一次 NET_ECMS_Init(); // 2. 设置回调:注册/心跳/报警走消息回调,码流走数据回调 NET_ECMS_SetCallBack(MSG_CB, STREAM_CB, NULL); // 3. 启动监听,阻塞式,通常放独立线程 NET_ECMS_StartListen("0.0.0.0", 7660); // 4. 进程退出前 NET_ECMS_StopListen(); NET_ECMS_Fini();消息回调里至少要处理三类事件:设备注册上来、设备心跳、设备下线/异常。这三类事件驱动整个状态机,是最核心的一块。
设备注册上来时,回调参数里会带设备 ID、源 IP、端口、通道列表一类信息。这里要做的事:查映射表 → 更新在线状态 → 记录最后一次心跳时间 → 如果是新设备,触发"是否自动取流"的策略。我建议注册回调里只做轻量的状态更新,不要在里面同步调用取流接口,原因后面第 5 章会讲,那是死锁的高发区。正确做法是把设备 ID 丢进一个队列,由工作线程消费,异步发起取流。
心跳的处理就是刷新时间戳。注意心跳间隔设备侧可能不一致,别把超时阈值卡得太死,一般设成心跳间隔的 3 倍比较稳。设备网络抖动很常见,一次心跳丢了就判离线会导致状态疯狂翻转,下游的取流、录像、告警逻辑全跟着抖。
2.3 心跳超时、离线判定与重连后的状态收敛
状态管理这块,我用的是"三态 + 去抖":
- 在线:最近一次心跳在阈值内;
- 可疑:超过一个心跳周期没收到,但还没到超时阈值,此时只打日志不发事件;
- 离线:超过超时阈值,触发下线事件,停止取流,释放资源。
去抖的精髓在于下线事件只发一次。很多实现是每次检查发现超时就发一次事件,结果一台设备掉线,下游收到几十条离线通知,重复停止取流、重复释放句柄。用一个last_state字段判断状态是否真的变化,变了才发事件。
重连后的状态收敛更麻烦。设备断线重连后,注册回调会再来一次,但这次设备 ID 还是那个 ID,通道还是那些通道。你需要保证:旧的取流会话已经彻底停掉,新的取流会话重新建立,不能出现同一路流有两个会话在推。我的处理方式是给每个会话分配一个自增的 session_id,重连时对比 session_id,旧的一律强制 stop,然后重建。这个坑很隐蔽——旧会话没停干净的话,流媒体服务器上会收到两路推流,表现为播放器画面周期性跳回几秒前,也就是所谓的"鬼影回放"。
3. 取流回调里到底吐的是什么:私有帧头与 PS 流的拆解
3.1 StartGetRealStream 的参数里藏着主码流与子码流的选择
取流接口的参数通常包括设备句柄、通道号、码流类型、回调函数和用户数据。码流类型这个参数最容易被忽略,但它直接决定了你的带宽账。
海康设备一般有主码流和子码流。主码流是高清的,1080P 起步,码率可能 4~8 Mbps,适合录像和上墙;子码流是标清的,码率 512 Kbps 到 1 Mbps,适合多路预览和移动端。中间件如果只输出一路,那要看业务:如果是给人看的预览墙,用子码流,CPU 和带宽都省一大截;如果是给 AI 分析用,用主码流,不然小目标根本看不清。
我的做法是两种都取,输出成两路流,业务侧按需拉。听起来资源翻倍,实际上子码流那路几乎不占 CPU(纯转封装),多出来的是带宽。你可以按"是否需要 AI 分析"来做开关,不需要的设备只取子码流。
还有个参数是取流超时。设太短会在网络抖动时频繁失败,设太长会在设备真挂掉时一直挂着不返回,工作线程被占死。我的经验值是 10 到 15 秒,配合上一层的重试。
3.2 从私有包到 PS 到 ES:两层剥离
回调给你的是一段一段的数据块,不是完整的一帧。数据块的头部是私有帧头,里面有包头标识、数据长度、帧类型、时间戳、通道号这些字段。不同协议版本帧头长度不一样(EHome 2.0 和 5.0 就不同),所以第一步一定要按文档解析帧头,用长度字段判断这个块是不是完整的。
私有头的后面,通常是 PS 流。PS 是 MPEG-2 Program Stream,里面打包了 PES 包,PES 里才是 H.264/H.265 的 ES(Elementary Stream)。这个结构在家用场景里也常见,DVD 的 VOB 文件就是 PS。
拆解路径有两条:
- 交给 FFmpeg:把剥离了私有头的数据直接喂给 FFmpeg 的
mpegps解复用器,它会自动吐出 H.264/H.265 包。这是最省事的做法,兼容性好,各种非标打包它都见过。缺点是你看不到中间过程,出了问题不好定位。 - 自己解 PS:解析 pack header、system header、PES header,把 payload 拼起来得到 NALU。可控,但 PES 头的各种变体(有的带 PTS,有的不带,长度字段可能是 0 表示不定长)会让你掉一层皮。
如果 SDK 支持让回调直接返回**裸流(ES)**而不是 PS,那就偷着乐吧,直接跳过这一层。取流参数里多试试,很多版本是有这个选项的。
不管走哪条路,都要处理粘包和半包。TCP 是字节流,SDK 回调也不保证一次给你一个完整包。做法是维护一个环形缓冲区,按帧头的长度字段累积数据,凑够一个完整包再往下游送。缓冲区要设上限,超了就丢最老的数据并计数告警,否则一个慢消费者能把你内存吃光。
3.3 攒帧、时间戳与队列水位
剥离完的裸流交给下游之前,有两件事必须做对。
第一件是攒帧。从 PS 里解出来的是一堆 NALU,不是可播放的帧。你需要按 NALU 类型判断帧边界:H.264 里 type 1 和 5 是 VCL NALU(5 是 IDR 关键帧),type 7(SPS)和 8(PPS)是参数集,看到 SPS 开头就意味着一帧要开始了,看到下一个 SPS 或者新的 slice 就说明上一帧结束。H.265 的 NALU 类型编码方式不同(用 6 bit),要单独处理。这里做错的表现很典型:播放器能出画面但每隔几秒花一下,或者直接黑屏。
第二件是时间戳。别用设备给的时间戳直接往标准容器里塞,设备的时钟不靠谱。我的做法是:第一帧到达时记一个基准时间(本地单调时钟),后续每帧按设备的相对增量推进,同时做一个单调性检查——如果新时间戳比上一个小了(设备重启、时钟跳变),就重置基准,让时间轴重新从当前开始。这样输出到 FLV/TS 的时间轴永远是单调递增的,播放器不会因为时间倒流而卡死。
队列水位是运维的命脉。中间件内部一般是"回调线程写入队列 → 封装线程从队列读走"。如果封装线程因为推流阻塞变慢了,队列就会涨。这时候你有两个选择:丢帧(丢非关键帧,保关键帧)或者主动断流重建。我倾向于设两个水位线:软水位(比如 200 帧)开始丢非关键帧并打警告日志,硬水位(比如 500 帧)直接断流重连。宁可让用户看到一次黑屏重连,也不要让延迟越积越大——延迟堆到 30 秒的直播,不如没有。
4. 三条出口的封装取舍:RTSP、HTTP-FLV、HLS
4.1 自研 muxer 还是交给 FFmpeg / 流媒体服务器
这是整套系统里最需要冷静做决定的地方。我看到过不少团队一上来就自己写 RTP 打包和 FLV 封装,写了三个月,能播了但死活不稳定,最后又换回 FFmpeg。
判断标准很简单,问自己三个问题:
- 你的并发路数会不会超过 50 路?不会的话,FFmpeg 进程方案完全够用;
- 你对首屏延迟的要求是不是低于 500 毫秒?不是的话,不需要自研;
- 团队里有没有人对 MPEG-TS、RTP、FLV 这些格式的内部结构门儿清?没有就别自研。
我的实际选择是:转封装用 FFmpeg,分发用流媒体服务器。中间件把剥离好、攒好帧的码流喂给 FFmpeg(早期用子进程管道,规模上来后改成 in-process 调 libavformat),FFmpeg 负责 copy 视频、转码音频、封装成 FLV/TS,然后推进流媒体服务器。流媒体服务器再把一路输入分发成 RTSP、HTTP-FLV、HLS、WebRTC 多路输出。
用进程方案时,典型命令长这样:
ffmpeg -probesize 512k -analyzeduration 0 \ -fflags nobuffer -flags low_delay \ -f mpegps -i pipe:0 \ -map 0:v:0 -c:v copy \ -map 0:a:0 -c:a aac -ar 8000 -ac 1 -b:a 32k \ -f flv rtmp://127.0.0.1:1935/live/cam_1001几个参数值得解释:-analyzeduration 0和-probesize配合,是为了减少首屏等待,默认 FFmpeg 会先攒一段数据做流分析,直播场景下这段等待纯属浪费;-c:v copy是关键,视频一路不转码,CPU 占用能压到 1% 以内;音频那里后面细说。
-fflags nobuffer和-flags low_delay是低延迟组合,但要注意它们和某些容器不兼容(比如 HLS 就不太吃这套),按输出格式分别配置。
4.2 RTSP 出口的 RTP 打包与传输方式
RTSP 这条出口最适合局域网内的大屏、NVR、以及需要低延迟的第三方系统对接。如果走流媒体服务器分发,你其实不用自己写 RTP 打包,服务器会做。但有几个点要提前定:
传输方式是 TCP 还是 UDP。UDP 延迟低,但丢包会导致花屏;TCP 不丢包,但在跨公网、弱网环境下会因为重传导致延迟堆积。我的默认建议是内网用 UDP,跨公网用 TCP,并且让客户端可配。有些老播放器只支持一种,提前问清楚。
SDP 里的参数集怎么给。H.264 的 SPS/PPS 要么放在 SDP 的sprop-parameter-sets里,要么在 RTP 流里用 FU-A 分片带着走(STAP-A 也可以)。放 SDP 里的好处是播放器一上来就有参数集,首帧快;坏处是设备切分辨率或者切码流后 SDP 没更新,画面就废了。我的做法是两者都给,SDP 里给一份,流里也带关键帧前的 SPS/PPS,兼容性最好。
RTSP 地址的规整。对外暴露的地址要统一格式,比如rtsp://ip:554/live/cam_1001,其中cam_1001是中间件分配的流 ID。流 ID 和设备的对应关系存在数据库里,业务系统只认流 ID,不认设备序列号,这样设备换了不影响业务。
4.3 FLV 与 HLS 的延迟、GOP 与浏览器兼容账
这两条出口的服务对象都是浏览器,差别在于延迟和兼容性。
HTTP-FLV延迟可以做到 1~3 秒,做法是把 FLV 数据挂在 HTTP 长连接上持续吐。优点是延迟低、实现简单、兼容性好(桌面 Chrome/Edge 没问题)。缺点是移动端浏览器不能播,因为 iOS Safari 不认 FLV。用 flv.js 这类前端库在浏览器里解封装渲染是常规方案。
HLS延迟通常 5~15 秒,延迟来自切片。切片的配置是门手艺:
ffmpeg -f mpegps -i pipe:0 -c:v copy -c:a aac -ar 8000 -ac 1 \ -f hls -hls_time 2 -hls_list_size 4 \ -hls_flags delete_segments+append_list+omit_endlist \ -hls_segment_type fmp4 \ /var/www/hls/cam_1001/index.m3u8-hls_time 2是切片目标时长,但实际切片长度取决于关键帧间隔(GOP)。HLS 规范要求每个切片从关键帧开始,如果你的设备 GOP 是 4 秒而切片设 2 秒,实际每个切片还是 4 秒,hls_time白设。所以正确姿势是:先看设备的 GOP 是多少,把切片时长设成 GOP 的整数倍。设备 GOP 可配的话,设成 2 秒、切片也设 2 秒,延迟和流畅度最平衡。
-hls_list_size 4是播放列表里保留的切片数,配合delete_segments自动清理老切片,不然磁盘会被写满。这是新手最常踩的坑之一——跑了两天发现磁盘 100%,一看 HLS 目录存了几十万个 ts 文件。
-hls_segment_type fmp4用 fMP4 切片而不是 TS 切片,主要是为了兼容 H.265。TS 封装对 H.265 的支持很有限,很多播放器播不了;fMP4 对 H.265 友好得多。代价是兼容性略差一点点,老设备可能不认。如果设备是 H.264,用默认的 TS 就行。
5. 音频、H.265 与时间戳:三个最容易翻车的细节
5.1 G.711 到 AAC:什么时候必须转,什么时候能省
海康这批 E-home 设备,音频编码大概率是 G.711A 或者 G.711U。问题来了:FLV 容器不支持 G.711,它只认 AAC 和 MP3。所以你要输出 HTTP-FLV,音频就必须转码。
好消息是音频转码非常便宜。8 kHz 单声道、32 kbps 的 AAC,一路的 CPU 占用大概只有视频软解的百分之一。所以该转就转,别为了省这点资源去折腾。
RTSP 就宽松得多,G.711A/G.711U 在 RTP 里有静态 payload type(分别是 8 和 0),可以直接打包,不需要转码。所以如果你的场景是局域网 RTSP 播放,音频直接 copy,一路零转码,那是真省。
HLS 走 TS 切片时,G.711 也能塞进 TS 里,但浏览器端解码支持不好,建议还是转 AAC。
还有一个经常被忽略的点:有些设备根本没有音频输入,或者音频开关默认关闭。这时候 FFmpeg 如果强行-map 0:a:0会直接报错退出。稳妥的做法是加-map 0:a:0?(问号表示可选),有就带,没有就纯视频输出。这个小符号救了我不少次。
5.2 H.265 在 FLV 上的尴尬
H.265(HEVC)压缩效率比 H.264 高 30%~50%,设备默认开着,尤其是 4G 摄像头,为了省流量基本都用 H.265。
但 H.265 在浏览器这块的兼容性一直是个麻烦:传统 FLV 格式的VideoTag里,CodecID 只定义了 H.264,H.265 需要走 Enhanced RTMP / Enhanced FLV 那套新规范,得播放器和服务器都升级到位才认。HLS 用 fMP4 切片能播 H.265,但 Safari 支持好、Chrome 要看版本。RTSP 天然支持,只要客户端解得了。
所以输出策略我一般是这样的:
| 输出格式 | H.264 | H.265 |
|---|---|---|
| RTSP | 完全没问题 | 完全没问题,取决于客户端 |
| HTTP-FLV | 完全没问题 | 需要服务器和播放器都支持 Enhanced FLV,否则不行 |
| HLS (TS) | 完全没问题 | 兼容性差,不建议 |
| HLS (fMP4) | 可以 | 可以,但要测播放器 |
如果设备是 H.265 而业务必须用 HTTP-FLV,只剩两条路:换子码流(很多设备子码流是 H.264)、或者视频转码成 H.264。转码的代价很大,1080P 一路软转大概要吃一个核,50 路就是 50 核,得上硬件加速卡。这个账一定要在方案阶段算清楚,别做到一半发现服务器扛不住。
5.3 设备重启后的时间戳回退处理
这是我踩过最深的坑,没有之一。
设备断电重启后,它输出的 PTS 会从零开始。如果中间件老老实实把设备 PTS 透传给容器,播放器就会看到时间戳突然倒退了几个小时,表现是画面卡住不动,或者疯狂追帧然后花屏。用 FLV 的话还可能直接触发播放器的 seek 逻辑,跳到开头重播。
处理思路前面提过,这里给个更具体的规则:
- 第一帧进来时,记
base_local = 本地单调时钟、base_dev = 设备 PTS; - 后续每帧输出时间戳
= base_local + (dev_pts - base_dev),单位做 90kHz 到 ms 的换算; - 每次收到新帧时做检查:如果
dev_pts < base_dev(设备重启了)或者dev_pts - last_dev_pts > 某个阈值(比如 5 秒,说明有长时间断流),就重置基准,把base_local更新为当前本地时间、base_dev更新为新的设备 PTS; - 输出严格单调,宁可中间有个小跳变,也不能倒退。
阈值那个 5 秒要按业务调。如果是网络抖动导致的偶发大间隔,重置基准会让画面时间轴轻微错位,但比时间戳倒流好得多。
还有一个隐藏问题:关键帧对齐。时间戳重置之后,如果下一个到达的帧不是 IDR,播放器拿着没有 SPS/PPS 的裸片是解不出来的。所以重置基准的同时,最好主动请求一次关键帧(如果 SDK 有这个接口),或者干脆把这段数据丢掉,等下一个自然关键帧再开始输出。后者实现简单,代价是最多等一个 GOP(2~4 秒)。
6. 从能播到稳定:我踩过的坑与排查顺序
6.1 黑屏、花屏、卡顿的分层排查法
出问题的时候不要到处瞎改,按层往下走,十分钟能定位到八成的故障。
第一层,设备注册上了吗?看中间件的注册日志。没有注册记录,后面全白搭。检查设备端填的地址端口、防火墙、以及中间件是不是真的在监听。我遇到过一次是云服务商的安全组忘了放行,设备侧显示"注册成功"(其实只是 TCP 连上了),但一个心跳都收不到。
第二层,取流请求发出去有没有回?日志里看取流接口的返回码,非零就去查错误码表。常见的失败原因是通道号不对(有的设备通道从 33 开始,不是 1)和码流类型不支持。
第三层,回调有数据吗?打点统计每秒收到的字节数和包数。有数据但很少,可能是设备在发呆或者码流类型选错。完全没有数据,是取流会话没建立成功。
第四层,数据能解出帧吗?如果 FFmpeg 一直在报 "Invalid data found when processing input" 或者疯狂丢包,说明私有头剥离有问题,或者 PS 解析位置错了。最快的验证方法:把回调收到的原始数据直接 dump 成文件,用ffprobe去读。能读出流信息说明数据本身没问题,问题在你的处理逻辑;读不出来说明数据剥错了。这个 dump 文件是排查利器,我建议在调试模式下永远保留这个开关。
第五层,解出来的帧能播吗?到这一层基本就是参数集、时间戳、关键帧的问题了。花屏多半是 SPS/PPS 丢失或者 PS 解复用丢包;卡顿多半是时间戳异常;黑屏有声音多半是视频 NALU 类型判断错了,把非 VCL 的 NALU 当成帧送出去了。
6.2 回调线程里的死锁与内存增长
SDK 的回调线程是厂商给的,你在里面做的事情越少越好。我定了几条死规矩:
- 回调里不做网络 IO、不做文件 IO、不申请大块内存。收到数据立刻拷进预分配的缓冲区,扔进无锁队列,然后返回;
- 回调里绝不调用 SDK 的其他接口。这是死锁的头号来源。你在数据回调里调
StopGetRealStream,而 SDK 内部可能正持着同一把锁等你回调返回,双方互相等,进程就挂那儿了。要停止取流,一律由工作线程去做,回调线程只负责通知; - 缓冲区预分配、复用。每秒几百个包,每次都 malloc/free,跑一天内存碎片就够你受的。用内存池,池子大小按峰值码率算。
内存泄漏的排查方法很土但有效:跑起来后用top或者/proc/[pid]/status盯 RSS,同时用 valgrind 在测试环境跑一遍长时间回放。两个数据一对,泄漏点基本跑不掉。最常见的泄漏是队列里的数据包在断流时没清空——每断一次流泄一点,跑一周就是个几百兆。
6.3 多路并发的资源账与压测方法
单路跑通和多路稳定是两回事。上规模前先算三笔账:
CPU 账。纯转封装(视频 copy + 音频转 AAC)单路大概占 0.5%~2% 的一个核;如果视频要转码,1080P 软转单路 60%~100% 一个核。所以 100 路纯转封装,8 核机器能扛;100 路转码,得上十几台机器或者硬件加速。这个账决定你的硬件预算,越早算越好。
带宽账。每路流的带宽等于码率乘以输出份数。一路 2 Mbps 的子码流,如果同时给 RTSP、FLV、HLS 三个输出,理论上出口带宽是 2×3 Mbps(如果流媒体服务器按需分发,实际会低很多,因为没人看的时候不发)。内网分发和公网分发的账不一样,公网带宽是按峰值收费的,一定要做按需分发(有人看才推)。
句柄账。每路流至少涉及一个设备会话、一个取流会话、一个 FFmpeg 上下文、一个推流连接。1000 路就是几千个文件描述符,ulimit -n默认 1024 直接爆炸。上线前务必调高,并且把 FD 数量做成监控指标,因为FD 泄漏是渐进的,能跑一周不代表没泄漏。
压测的做法:先用脚本模拟 N 路取流(如果设备不够,用测试设备多开通道,或者用录好的码流文件循环喂给中间件),从 10 路开始,每加 10 路稳定跑 30 分钟,记录 CPU、内存、FD、队列水位。重点看内存是不是台阶式上涨(涨了就说明有泄漏),以及队列水位在高并发下是否稳定(持续上涨说明下游处理不过来)。
7. 部署与长期运维的一点个人经验
中间件的配置项会越来越多:监听端口、协议版本、超时阈值、码流类型、输出格式开关、切片时长、水位线……我的做法是所有配置集中在一个配置文件里,用配置中心下发,改完热加载不用重启。特别是超时阈值这类参数,现场网络质量千差万别,一个固定值肯定不够用,得允许按设备分组配置。
关于对外暴露的安全,两条底线:E-home 监听端口别裸奔在公网,至少加 IP 白名单,因为设备 ID 是设备自己报的,没有强认证的话伪造注册很容易;RTSP 和 FLV 出口加鉴权,URL 里带个有时效的 token,比裸地址安全得多,也方便统计每个客户端的拉流情况。
最后一点,也是我觉得最有价值的:给每路流留一个"照片兜底"。不管中间件怎么优化,总会有设备本身故障、网络彻底断掉的情况。这时候前端拿不到流,用户体验很差。我在中间件里加了个逻辑,如果某路流超过 N 秒没有关键帧,就把最近一帧的关键帧截图存下来,前端在拉流失败时展示这张图,并给个"设备离线"的提示。一个小功能,但客户投诉量明显少了很多——因为用户知道发生了什么,而不是对着黑屏怀疑系统坏了。
这套东西我从最早的 FFmpeg 子进程方案一路迭代过来,最大的体会是:私有协议接入的难点从来不是"能不能取到流",而是"取到流之后怎么把它稳住"。取到第一帧可能只需要一天,把它连续跑一个月不掉线、不花屏、不漏内存,才是真正的工程量。所以如果你正准备做这个,建议第一天就把 dump 开关、监控指标和日志分级做好,后面每踩一个坑,都能靠这些数据快速定位,而不是靠猜。