ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从海康E-home私有协议到RTSP/FLV:视频中间件接入与RTP推流实战

从海康E-home私有协议到RTSP/FLV:视频中间件接入与RTP推流实战 1. 先搞清楚需求为什么要做一层视频中间件视频中间件这个词听起来很虚但落到具体场景就非常实在手上有一批海康 E-home 系列的 IPC设备本身只认自家那套私有协议第三方平台、自研后台、大屏系统全都不认识它。你要么被绑死在官方 SDK 上要么就得自己写一层翻译官把私有协议里的裸码流掰开再吐成 FLV、HLS、RTSP 这些谁都能拉的标准流。我这个项目做的就是这么一层东西它不属于任何一个业务系统只负责接入、解析、转封装、分发做完了谁都能用。我把它定位成视频中间件是因为它真的像中间件上游是一堆不讲道理的私有协议设备下游是一堆各有脾气的播放器和平台。中间这层如果做薄了下游每接一个新客户端就得改一次代码做厚了又会变成另一个臃肿的 NVR。我的选择是只做三件事——把设备连上、把码流解出来、把标准流吐出去——其他一律不管。这样一来一个 40 路 E-home 摄像头的项目后端同事只需要拿一个 FLV 地址就能在浏览器里播前端不用装插件大屏不用装 ActiveX运维也不用再为某个平台只支持 RTSP 而头疼。适合看这篇内容的人大致有三类一是做安防集成、被私有协议卡过脖子的后端开发二是想给自研平台补齐能接海康家用设备能力的架构同学三是手上有一批老设备、想低成本盘活它们的技术爱好者。代码部分我会给 Python 侧的协议探测与解析示例流媒体侧以 ZLMediaKit 为主、FFmpeg 为兜底两条路都写清楚你可以只挑一条走。全文的协议细节我都是在几款常见固件上实测或复现过的但 E-home 这个东西不同批次固件差异不小凡是和你的设备对不上的地方按我在文中标注的方法去抓包验证别硬套。1.1 E-home 设备的接入困局E-home 系列设备最大的问题是封闭但便宜。它走的是海康自家的一套基于 TCP 的私有协议报文用类似 XML 的明文信令做握手和登录登录成功后直接在同一个 socket 上吐二进制码流。这套协议官方没有公开文档SDK 又基本只在 x86 的 Windows/Linux 上有完整支持你要在 ARM 板子、国产化服务器或者容器里跑就会非常难受。更麻烦的是很多 E-home 型号的主码流是 H.265子码流是 H.264音频有时候是 G.711A、有时候是 AAC甚至同一型号不同固件版本都能给你换个编码平台侧如果不做适配接上去就是花屏或者无声。我见过最典型的翻车场景是集成商拿 SDK 在 Windows 上跑通了交付时客户要求跑在 Linux 容器里结果 SDK 的 so 依赖一堆老版本库装上就冲突最后只能加一台 Windows 机器专门做转流成本直接翻倍。还有一种是设备数量一多SDK 回调线程模型撑不住掉线后不重连运维半夜被电话叫起来重启进程。这些问题的根子都在于你把协议适配和业务逻辑揉在一起了。拆开来看协议适配是一件边界非常清晰的事它就该被抽成独立的一层。1.2 中间件的定位只做协议翻译不做业务我一直坚持一个原则中间件的输出必须是通用物不能是业务物。什么叫通用物RTSP、RTMP、HTTP-FLV、HLS 这些就是通用物任何播放器、任何平台、任何语言都能消费。什么叫业务物比如给 A 客户返回带水印的流给 B 客户只推报警片段给 C 客户的流要叠加 OSD 文字——这些统统不该进中间件应该由下游平台自己去处理或者用独立的转码节点去做。这样切分的好处非常明显。第一中间件可以做得很薄、很稳代码量小意味着 bug 少、内存占用可控、重启速度快。第二下游的播放方式变了中间件不用动。我最早做的一版下游是 RTMP 推给直播服务器后来客户要求浏览器直接播只加了一个 HTTP-FLV 的输出配置一行业务代码没改。第三横向扩展简单一个中间件实例扛 30 路不够就多起几个实例按设备 ID 做分片天然就是分布式的。还有一个容易被忽略的点中间件要负责把设备的不确定性吃掉。设备会掉线、会重启、会因为网线松动丢包、会因为电源不稳导致时间戳跳变。这些在中间件里统一处理成对下游透明的行为——永远给下游一个稳定的流地址断了自动重连重连期间下游播放器看到的是等待而不是报错。下游代码里不需要写任何一行重连逻辑这才是中间件真正的价值。1.3 分层架构与选型取舍我最终的架构分五层从上到下依次是设备接入层、信令会话层、码流解析层、封装层、输出层。接入层管 TCP 连接、超时、保活会话层管登录、SessionID、取流指令的生命周期解析层负责拆包、区分音视频、提取时间戳封装层把裸 ES 数据打成 RTP 或者喂给 FFmpeg输出层交给媒体服务器去分发 RTSP/FLV/HLS。为什么这么分因为每一层的失败模式完全不同。接入层挂了是网络问题解析层挂了是协议适配问题输出层挂了是播放器兼容问题。分层之后日志一打就能定位到是哪一层出事排查效率差好几倍。我踩过的最深的坑就是在早期版本里把解析和输出写在一个循环里结果某台设备发了个畸形包整个进程直接崩连带其他 20 路全断。后来强制拆层解析层遇到异常包直接丢弃并计数再也不会把整个服务带崩。选型上媒体服务器我对比过三套自研 RTSP Server、MediaMTX、ZLMediaKit。自研的优点是可控缺点是 RTSP 的 RTP 打包、SDP 生成、TCP 交织模式、播放器兼容性这些细节太耗时间我评估至少要两周才能做到VLC 和 ffplay 都能拉。MediaMTX 部署极简配置文件友好适合小规模但高并发下的性能调优空间小。ZLMediaKit 是国内项目里用得最多的RTSP/RTMP/HTTP-FLV/HLS/WebRTC 一把梭还提供了 RTP 推流接口我最终选了它。FFmpeg 则作为兜底方案保留遇到编码特别奇怪的设备用管道喂给 FFmpeg 转一道虽然多占一点 CPU但兼容性最强。2. 吃透海康 E-home 私有协议接入层怎么落地协议这一层是整件事里最费时间的部分因为没文档只能靠抓包加试错。我的建议是先拿一台设备用 Wireshark 抓一次完整的打开客户端、登录、预览、关闭流程把报文抄下来再写脚本复现。一般情况下一次完整的取流流程只有三段信令探测、登录、开始取流剩下的就是纯二进制码流。这三段信令的报文格式在不同固件上略有差异但骨架是一致的理解了骨架就能举一反三。这里必须说一句合规的事这套东西只能用来接入你自己拥有或者已经获得明确授权的设备不要拿去做任何越权访问。协议里带着账号密码抓包文本里也就带着明文凭据工程上一定要把凭据放到配置中心或者环境变量里别硬编码进代码更别提交到代码仓库。我见过有人把带密码的抓包文件一起传上去了这是非常低级的失误。2.1 握手与设备发现第一包发什么E-home 设备默认监听在 TCP 8000 端口部分型号是 80 或者 554需要逐个试。连上之后客户端要主动发一个探测包告诉设备我是谁、我想问什么。报文是明文 XML形如import socket HOST 192.168.1.64 PORT 8000 PROBE ( ?xml version1.0 encodingutf-8? Probe Uuid8b1a9d0e-3f5c-4a2b-9c77-112233445566/Uuid Typesinquiry/Types /Probe ) def recv_until(sock, timeout3.0): sock.settimeout(timeout) buf b while True: try: chunk sock.recv(4096) except socket.timeout: break if not chunk: break buf chunk if b/ProbeResponse in buf: break return buf s socket.create_connection((HOST, PORT), timeout5) s.sendall(PROBE.encode(utf-8)) print(recv_until(s).decode(utf-8, errorsreplace))设备正常会回一个ProbeResponse里面带着设备类型、序列号、固件版本、通道数等信息。这个 UUID 是自己生成的随便一个合法格式的 UUID 就行设备只把它当作客户端标识。这里面有个细节有些固件要求探测包的 XML 声明和标签之间不能有换行否则直接不响应。我一开始用格式化字符串拼出来带了缩进换行调试了两个小时才发现是这个问题。所以拼报文时一定要用紧凑格式别为了好看加空格。如果探测包发出去没反应先别怀疑协议按顺序检查三件事端口对不对、设备是不是被别的客户端占着很多 E-home 设备同一时间只允许一个预览连接、防火墙有没有拦。我遇到过设备只能同时被一个客户端连接的情况用官方 App 连过之后脚本要等几十秒才能连上这个坑挺隐蔽的。2.2 登录鉴权与 Session 生命周期管理探测通了之后就是登录。报文同样是明文 XML带上用户名和密码LOGIN ( ?xml version1.0 encodingutf-8? LoginUser version1.0 UserNameadmin/UserName Password你的设备密码/Password /LoginUser )登录成功后设备返回的响应里会带一个SessionID后面所有指令都要带上它。这里有两个大坑。第一个坑是密码类型E-home 设备有两种凭据一种是设备的管理员密码另一种是机身贴纸上的六位大写字母验证码。不同固件要求不一样有的只认验证码有的两个都认试的时候两种都试一下。第二个坑是会话超时SessionID 不是永久的长时间不发数据会被设备回收表现就是流还在推但画面突然卡死不动。解决办法是在会话层维护一个状态机我用的策略是每 30 秒发一次保活指令连续三次没有响应就判定链路已死主动关闭 socket 重连。这里要注意重连不是简单地重新 connect而是要走完整的连接、探测、登录、取流流程把 SessionID 换掉。我早期图省事重连时复用了旧 SessionID结果设备返回成功但流是空的排查了很久。会话状态机的状态我一般设计成四个CONNECTING、AUTHED、STREAMING、BACKOFF。任何一层异常都往BACKOFF走退避重试的间隔用 1s、2s、4s、8s 最多到 30s 的指数退避避免设备刚重启就被疯狂重连打挂。这个细节看起来小但在几十路设备同时掉线又同时恢复的场景下不加退避会导致雪崩设备侧连接数瞬间打满。2.3 取流指令与码流包头拆解登录成功下一步发取流指令。常见的形式是START ( ?xml version1.0 encodingutf-8? StartStream version1.0 fSessionID{session_id}/SessionID /StartStream )这条指令发出去之后设备会先返回一个 XML 响应告诉你流的编码格式、分辨率、帧率、码率然后紧接着开始吐二进制数据。注意XML 响应和二进制数据是同一个 socket 上连续来的中间没有任何分隔符所以你的读取循环必须先判断这一段是 XML 还是二进制。我的做法是读到开头就按 XML 解析直到遇到对应的闭合标签其余一律按二进制流处理。这个判断如果做错了就会出现第一帧正常、后面全花的诡异现象。二进制数据的包结构是我实测下来比较常见的一种布局包头 16 字节后面跟载荷。字段大致是魔术字、包总长、载荷类型、通道号、时间戳。用 struct 解起来是这样import struct # 常见包头布局magic(4) 包长(4) 载荷类型(2) 通道号(2) 时间戳(4) HEADER_FMT 4sIHHI HEADER_LEN struct.calcsize(HEADER_FMT) # 16 def iter_packet(sock, bufb): while True: while len(buf) HEADER_LEN: chunk sock.recv(65536) if not chunk: return buf chunk magic, total_len, payload_type, channel, ts struct.unpack( HEADER_FMT, buf[:HEADER_LEN] ) if total_len HEADER_LEN or total_len 4 * 1024 * 1024: # 长度不合理丢弃一个字节重新找头防止解包错位 buf buf[1:] continue while len(buf) total_len: chunk sock.recv(65536) if not chunk: return buf chunk payload buf[HEADER_LEN:total_len] buf buf[total_len:] yield payload_type, channel, ts, payload这段代码里最关键的是那句长度不合理就丢一个字节重新找头。TCP 流是没有边界的一旦解析错位后面全都是垃圾数据如果不做重同步整个流就废了。这个滑动一个字节重新对齐的技巧在自定义协议的解析里非常常用比直接断开重连温柔得多。载荷类型一般用数字区分视频、音频、心跳或者信令。你不需要一开始就猜准每个数字的含义可以先把每种类型的前 64 字节打成十六进制日志看哪种类型的数据里出现了00 00 00 01或者00 00 01这种 H.264/H.265 的 NAL 起始码那基本就是视频。2.4 编码参数确认别急着封装拿到裸数据之后先别急着往 FFmpeg 里塞。第一件事是确认编码格式这一步做错的代价是后面所有工作白做。判断方法很直接看 NAL 起始码之后的第一个字节如果(byte 0x1F)的值在 1 到 23 之间大概率是 H.264H.265 的 NAL 头是两字节((byte 1) 0x3F)的取值范围不同SPS 对应的类型号是 33PPS 是 34IDR 是 19 或 20。最省事的办法其实是看取流响应里那段 XML很多固件会把VideoCodec直接写出来能用元数据就别硬猜。第二件事是确认有没有音频、音频是什么格式。这一步很容易被忽略因为大部分调试场景只看画面。G.711A 是最常见的采样率 8000Hz、单声道、每帧 160 或 320 字节也有 AAC 的带 ADTS 头每个 ADTS 帧开头是FF F1或FF F9。格式搞错了播放器要么没声音要么全是刺耳的噪声。我的建议是先把音频单独 dump 成文件用ffplay -f alaw -ar 8000 -ac 1 audio.pcm听一下确认对了再往管道里接。第三件事是拿到 SPS 和 PPS。这两个东西决定了播放器能不能正确初始化解码器。很多中途接入的播放器之所以黑屏就是因为错过了流开头的 SPS/PPS。解决办法有两个一是缓存住最新的 SPS/PPS在每次关键帧前面重新插入一份二是在媒体服务器侧配置定期重发。我两个都做了双保险。实测下来这样处理后播放器首帧时间从看运气变成稳定在 1 秒以内。3. 从裸 ES 到标准流封装与输出层设计裸的 H.264 ES 数据是没法直接给播放器的中间必须经过封装。封装层要做的事本质上是把没有时间基准、没有包边界、没有元数据的一串字节变成有 PTS/DTS、有 RTP 包头、有 SDP 描述的标准媒体流。这一层做得好不好直接决定了延迟、兼容性和并发上限。我这里给两条路主路是把 ES 打成 RTP 推给媒体服务器兜底路是用 FFmpeg 管道转封装。3.1 输出方案选型自研 RTSP Server 还是借力 Media Server先说结论除非你有非常特殊的定制需求否则别自研 RTSP Server。RTSP 看着简单实际坑极多。播放器发出的OPTIONS、DESCRIBE、SETUP、PLAY四步握手你要正确处理RTP 打包要按 MTU 分片FU-A 的起始位、结束位不能错TCP 交织模式下RTP 包前面要加$加通道号加长度SDP 里的sprop-parameter-sets要把 SPS/PPS 做 Base64还有 keepalive、TEARDOWN、多客户端同时拉同一个流等等。任何一个细节错了表现就是某个播放器能播、另一个不行排查起来非常痛苦。借力 Media Server 之后你只需要负责把码流按标准 RTP 喂进去剩下的分发、SDP、多协议转换全都由服务器处理。更重要的是媒体服务器通常已经处理好了一个流被多个客户端同时拉的场景。自研的话你得自己做流的分发和引用计数一个客户端断开就把流关掉另一个还在看的客户端瞬间就断了这种 bug 在测试时很难发现上线后必炸。3.2 用 RTP 推流把裸流喂给 ZLMediaKitZLMediaKit 提供了一个 RTP 推流接口流程是先用 HTTP API 开一个 RTP 接收端口拿到端口号之后自己把 H.264 打包成 RTP 发过去服务器会自动把它变成一路可以拉的标准流。开端口curl http://127.0.0.1:80/index/api/openRtpServer?secret你的secretport10000tcp_mode1stream_idcam01返回{code:0,port:10000}就说明端口开好了。tcp_mode1表示用 TCP 接收包前面要加 4 字节长度前缀这样不用担心 UDP 丢包tcp_mode0是 UDP延迟低一点点但容易花屏内网环境我一般用 TCP。打包 RTP 的核心逻辑是把每个 NAL 单元按 1400 字节左右切片用 FU-A 分片import struct RTP_HEADER struct.Struct(BBHII) def pack_rtp(nal: bytes, seq: int, ts: int, ssrc: int, marker: bool, pt: int 96) - bytes: header RTP_HEADER.pack( 0x80, # V2, 无填充、无扩展、无CSRC (pt 0x7F) | (0x80 if marker else 0x00), seq 0xFFFF, ts 0xFFFFFFFF, ssrc, ) max_payload 1400 if len(nal) max_payload: return header nal # FU-A 分片 nri nal[0] 0x60 nal_type nal[0] 0x1F body nal[1:] out b chunks [body[i:i max_payload] for i in range(0, len(body), max_payload)] for idx, chunk in enumerate(chunks): start 1 if idx 0 else 0 end 1 if idx len(chunks) - 1 else 0 fu_indicator nri | 28 fu_header (start 7) | (end 6) | nal_type out header bytes([fu_indicator, fu_header]) chunk return out时间戳这一块是重点。RTP 的时间戳单位是 90000Hz也就是每秒钟增加 90000。如果设备给的是毫秒时间戳换算就是ms * 90。但设备的时间戳经常不靠谱——有的设备重启后从零开始有的会突然跳变有的同一帧的多个分片时间戳还给你微调。我的处理方式是丢掉设备的原始时间戳自己维护一个单调递增的时钟以第一个包为基准后续按本地单调时钟计算增量然后换算成 90kHz。这样下游播放器看到的时间戳永远是平滑的画面上不会出现忽快忽慢。实测这一招解决了我遇到过的 80% 的播放器进度条乱跳问题。推流的 socket 写入部分TCP 模式下每条 RTP 包前面加 4 字节大端长度def send_rtp(sock, packet: bytes): sock.sendall(struct.pack(I, len(packet)) packet)推上去之后你就能拉到这几路地址协议地址形式典型延迟适用场景RTSPrtsp://127.0.0.1/live/cam010.3~1s专业客户端、NVR、ffplayRTMPrtmp://127.0.0.1/live/cam011~3s直播推流、老式平台HTTP-FLVhttp://127.0.0.1/live/cam01.live.flv1~2s浏览器、flv.jsHLShttp://127.0.0.1/live/cam01/hls.m3u85~10s移动端、兼容性优先WebRTChttp://127.0.0.1/live/cam01.live.mp40.2~0.5s超低延迟浏览场景3.3 FFmpeg 兜底方案管道封装与参数调优有些设备的编码比较古怪或者音频是 G.711A 需要转码RTP 手打包就不太合适了。这时候用 FFmpeg 管道最省事。把视频 ES 从标准输入喂进去音频从另一个文件描述符喂进去输出直接推到 RTMPffmpeg -hide_banner -loglevel warning \ -fflags nobuffer -flags low_delay \ -probesize 32 -analyzeduration 0 \ -f h264 -i pipe:0 \ -f alaw -ar 8000 -ac 1 -i pipe:3 \ -c:v copy -c:a aac -ar 8000 -ac 1 -b:a 32k \ -f flv rtmp://127.0.0.1/live/cam01几个参数值得解释一下。-probesize 32和-analyzeduration 0是为了让 FFmpeg 不要在开头缓冲太多数据再开始输出默认值会让你多等好几秒才出画面。-fflags nobuffer关掉输入缓冲-flags low_delay让编码器走低延迟路径虽然这里是 copy 不重编码但对某些格式仍然有效。-c:v copy是关键直接复制码流不重编码CPU 占用几乎为零如果你把这里改成-c:v libx264单路 1080p 就会吃掉一到两个核心30 路的机器根本扛不住。音频从pipe:3读是因为 FFmpeg 一个进程只能从 stdin 读一路-i第二路输入得用额外的文件描述符。Python 里可以用subprocess.Popen(pass_fds(fd,))配合os.pipe()来实现。如果你不需要音频直接把音频那两行删掉就行简单得多。我建议第一版先只做视频跑通了再加音频出问题的时候排查范围小一半。3.4 三路输出的参数取舍与延迟账HLS 的延迟是很多人最关心的问题这里算一笔账。HLS 的工作方式是切片播放器至少要拿到一个完整的切片才能开始播还要预取一两个切片做缓冲。如果你配置segDur2、segNum3那么理论最坏延迟大约是(segNum 1) × segDur也就是 8 秒左右实测通常在 5 到 7 秒。把segDur降到 1、segNum提到 5延迟能压到 3 到 5 秒代价是切片文件数量翻倍对存储和请求数有压力。如果业务要求延迟低于 2 秒别用 HLS直接用 HTTP-FLV 或者 WebRTC。HTTP-FLV 的延迟主要来自 GOP 长度和播放器缓冲。设备主码流的 GOP 一般是 1 到 2 秒也就是关键帧间隔 25 到 50 帧。播放器必须等到关键帧才能开始解码所以首帧时间基本等于 GOP 长度加上网络传输时间。如果你的业务对首帧特别敏感可以在设备侧把 GOP 调到 25 帧1 秒代价是码率会略微上升因为 I 帧比 P 帧大得多。这一块的调优没有银弹只能根据你的带宽和延迟要求做取舍。RTSP 的延迟是最低的因为它是纯流式传输没有切片和缓冲的概念。实测在局域网里能做到 300 毫秒以内。但 RTSP 的客户端兼容性比不上 HTTP-FLV浏览器原生不支持移动端支持也不一致。我的做法是三路同时输出让下游自己选中间件这边只是多几个配置项的事媒体服务器的资源开销增加非常有限因为底层只有一路流多协议输出只是换了个壳。4. 完整实操从环境准备到浏览器看到画面前面讲的是原理和细节这一节把整个流程串起来走一遍。我假设你在一台 Ubuntu 22.04 的机器上操作设备是一台常见的 E-home IPC网段和机器在同一局域网。整个流程走完你应该能在浏览器里看到一个 HTTP-FLV 的实时画面。4.1 环境与依赖先装基础依赖。Python 用 3.10 以上就行主要是用它的 socket 和 subprocess不需要额外的重型库。媒体服务器我选 ZLMediaKit 的编译好的二进制包直接下载解压就能跑sudo apt update sudo apt install -y python3 python3-pip ffmpeg netcat-openbsd # 拉取 ZLMediaKit 的 release 包并解压 mkdir -p /opt/media cd /opt/media # 这里按官方 release 页面选择对应架构的压缩包解压后结构如下 # /opt/media/ZLMediaKit/ # ├── bin/MediaServer # ├── conf/config.ini # └── www/启动前一定要改conf/config.ini默认配置有几个地方必须动。[rtsp]段的port确认是 554 或者改成高位端口避免权限问题[hls]段的segDur改成 2 或者 1[general]段的secret换成你自己的密钥后面调 API 要用[http]段的port是 80如果你机器上已经有服务占用了改成别的。改完启动cd /opt/media/ZLMediaKit ./bin/MediaServer -d 然后curl http://127.0.0.1/index/api/getServerConfig?secret你的secret验证一下接口通不通。这个 secret 一定要改默认值全网都知道暴露在公网上很容易被人随便调接口。4.2 先用探针脚本抓一段原始码流在写完整中间件之前我强烈建议先写一个只有几十行的探针脚本把裸数据 dump 到文件里。这样做的好处是你能亲眼看到协议长什么样后面解析出问题时有原始数据可以对照。脚本逻辑就是前面那几段代码拼起来连接、探测、登录、取流然后把收到的二进制写进文件同时把 XML 响应打印出来。import socket, struct, time HOST, PORT 192.168.1.64, 8000 DUMP open(raw_dump.bin, wb) s socket.create_connection((HOST, PORT), timeout5) s.sendall(PROBE.encode()) print(PROBE RESP:, recv_until(s).decode(utf-8, replace)) s.sendall(LOGIN.encode()) resp recv_until(s) print(LOGIN RESP:, resp.decode(utf-8, replace)) sid extract_session_id(resp.decode(utf-8, replace)) s.sendall(START_TEMPLATE.format(session_idsid).encode()) head recv_until(s) print(STREAM INFO:, head.decode(utf-8, replace)) s.settimeout(20) total 0 t0 time.time() try: while time.time() - t0 15: data s.recv(65536) if not data: break DUMP.write(data) total len(data) except socket.timeout: pass print(抓取字节数:, total) DUMP.close()跑完之后用xxd raw_dump.bin | head -50看前几百字节你能看到 XML 结束的位置和二进制数据的开头也能找到00 00 00 01的 NAL 起始码。这一步能帮你确认包头长度、字节序和载荷类型比对着文档猜快得多。我第一次做的时候就是靠这一步发现设备的包头长度不是我以为的 12 字节而是 16 字节避免了后面所有解析全错的惨剧。4.3 启动中间件与验证每一路输出探针跑通之后把解析、RTP 打包、推流三段接起来就是一个能用的中间件了。启动后按顺序验证每一路输出出问题好定位。RTSP 用 ffplay 验证ffplay -rtsp_transport tcp -i rtsp://127.0.0.1/live/cam01加上-rtsp_transport tcp是强制走 TCP 传 RTP避免 UDP 丢包导致的花屏。如果是内网且带宽充裕去掉这个参数延迟会低一点。HTTP-FLV 用 ffplay 或者 VLC 验证ffplay -i http://127.0.0.1:80/live/cam01.live.flvHLS 验证的关键是看 m3u8 有没有在持续更新curl -s http://127.0.0.1:80/live/cam01/hls.m3u8 | tail -5 sleep 5 curl -s http://127.0.0.1:80/live/cam01/hls.m3u8 | tail -5两次输出如果#EXT-X-MEDIA-SEQUENCE在递增说明切片在正常滚动用ffplay拉一下就没问题。浏览器里验证 HTTP-FLV 最简单的方式是写一个几行的 HTML 页面引入 flv.js把地址填进去video标签就能播了。4.4 用 systemd 做成常驻服务中间件跑起来之后必须做成常驻服务不然 SSH 一断就没了。systemd 的配置要点是重启策略和日志[Unit] DescriptionEhome Video Middleware Afternetwork-online.target [Service] Typesimple WorkingDirectory/opt/ehmid ExecStart/usr/bin/python3 /opt/ehmid/main.py --config /opt/ehmid/config.yaml Restartalways RestartSec5 StandardOutputappend:/var/log/ehmid/stdout.log StandardErrorappend:/var/log/ehmid/stderr.log LimitNOFILE65535 [Install] WantedBymulti-user.targetLimitNOFILE这一项很关键。每一路设备至少占一个 socket加上下游拉流、媒体服务器内部的连接几十路轻松超过默认的 1024 文件描述符上限超了之后新连接全失败报错还很不明显。我踩过一次加到 65535 之后就再没出过问题。Restartalways配合代码里的异常兜底基本能保证中间件不会彻底挂掉。5. 踩坑记录与问题排查速查这一节是我实际项目里攒下来的问题清单。视频这块的排查特别依赖分段定位所以我按照连接层、码流层、播放层三段来分出问题的时候先判断在哪一段能省很多时间。5.1 连接与鉴权类问题最常见的是登录失败。表现是发了登录报文设备要么不回要么回一个错误码。先确认密码类型管理密码和机身验证码都试一遍再确认是不是有别的客户端占着连接很多 E-home 设备限制同时预览路数官方 App 开着的时候脚本连不上最后看设备的登录失败锁定策略有些设备连续失败几次会锁定几分钟你越是疯狂重试越连不上这时候要停下来等一会儿。第二种是连上了、登录成功了、但流是空的。这个绝大多数是取流指令的参数不对或者 SessionID 传错了。有个隐蔽的坑是设备返回的 SessionID 里可能带前后空格或者换行你如果直接拼进 XML 又不 strip设备就会认为 SessionID 无效但它不回错误只是不给你流。这种坑只能靠打日志解决我现在的习惯是所有信令的收发都按十六进制加文本双份打印出问题一眼就能看出有没有多余字符。第三种是长时间运行后掉线。表现是运行几个小时到一天不等突然所有通道全断。原因通常是心跳没发或者发得太少。我的策略是 30 秒一次保活同时监控 socket 的读超时超过 90 秒没收到任何数据就直接重连不要等 TCP 自己发现连接异常因为在设备直接掉电重启的场景下TCP 可能几分钟都不会报错。5.2 码流解析类问题花屏、绿屏、画面撕裂基本都能归到解析层。第一要查的是 NAL 边界有没有对齐。用前面 dump 出来的原始数据用十六进制工具看看00 00 00 01的位置如果每隔一段就能找到说明起始码识别没问题如果一片混乱那就是解析错位了检查包头长度和字节序。第二要查的是 SPS/PPS 有没有丢。表现是首帧能出来播几秒之后花屏或者黑屏重新拉动进度条又好了。这是典型的解码器丢了参数集。解决办法是缓存最新的 SPS/PPS在每个 IDR 帧前面重新插进去。判断 IDR 帧的方法就是看 NAL 类型H.264 里类型 5 是 IDRH.265 里类型 19 和 20 是 IDR。第三是时间戳跳变。表现是画面忽快忽慢或者播放器进度条乱跳。原因前面说过设备时间戳不可靠。解决办法就是别信设备的时间戳自己用本地单调时钟生成。这个改动只有几十行代码但效果非常明显。我做这个改动之前客户反馈画面一卡一卡改完之后就没人提了。5.3 播放端类问题HLS 延迟大、切片不更新先看config.ini里的segDur和segNum再看有没有开启segKeep之类保留切片的配置切片被立刻删掉的话播放器会反复 404。FLV 首帧黑屏看 GOP 长度如果设备 GOP 设成了 100 帧4 秒首帧就要等 4 秒这个在设备侧或者中间件侧的转码节点调都行。RTSP 拉不到流先确认媒体服务器有没有收到 RTP用curl查一下getMediaList如果列表里没有你的流说明推流这一步就没成功往上游查。还有一个特别容易忽略的问题同一个stream_id被重复推流。表现是画面来回切换或者花屏。原因是中间件重连之后没有先关闭旧的流两路数据同时往一个 stream_id 上打。解决办法是在重连前先调close_streams接口把旧的关掉或者让 stream_id 带上一个递增的序号。我现在的做法是 stream_id 固定但重连时强制关闭旧流简单可靠。5.4 常见问题速查表现象大概率原因排查动作处理方式探测包无响应端口错误/设备被占用换 8000/80/554 试连关闭其他客户端重试登录返回失败密码类型不对/被锁定换验证码再试停止重试等待解锁连上但无码流SessionID 带空白字符打印信令十六进制strip 后重发指令首帧黑屏GOP 过长/缺 SPS/PPS看关键帧间隔缩短 GOP缓存参数集播放几秒后花屏参数集丢失/时间戳跳变抓包看 NAL 序列关键帧前重插 SPS/PPS画面忽快忽慢设备时间戳不稳打印时间戳变化改用本地单调时钟HLS 延迟超过 10 秒切片时长过大查 segDur/segNum调到 1 秒、5 片拉流地址 404流未推上去/被关掉查媒体列表修复推流重连前关旧流运行数小时后全断心跳缺失查保活日志30 秒保活90 秒超时重连新连接全部失败文件描述符耗尽查 ulimit提到 655356. 稳定性、性能与规模化的几个关键点能跑通和能上线是两码事。我做的第一个版本在自己机器上跑得很欢一上生产环境就各种问题。这一节讲的是从能用到敢让客户用之间的那些事。6.1 状态机与断线重连中间件最核心的稳定性设计就是状态机。我前面提过四状态的设计这里补充一下细节。每个设备通道一个独立的协程或者线程状态机在这个上下文里跑。任何一步异常都不允许抛出到顶层全部在状态机内部消化转成状态转移。日志里记录每一次状态转移的原因和时间戳这样出问题时看日志就能画出一条时间线非常直观。重连的退避策略要区分情况。网络不可达的情况退避时间长一点1s 到 30s 指数增长设备主动断开的情况可以稍微激进一点因为它可能只是重启了几秒就回来。另外重连次数要有上限报警如果一台设备连续十次都连不上那大概率是设备本身出了问题需要通知运维去看而不是无限重试。我加了一个连续失败超过 N 次触发告警的机制帮客户提前发现了好几台坏掉的设备。6.2 并发规模与资源估算资源估算这件事必须在上线前算清楚不然扩容的时候会很被动。我按 1080p、25fps、H.264 主码流、平均 4Mbps 来算。带宽上40 路设备的接入总带宽是 40 × 4Mbps 160Mbps加上下游拉流如果每个流被两个客户端拉就是 320Mbps。一台千兆网卡的机器理论 1000Mbps考虑实际效率打个七折700Mbps还留有余量但如果是 100 路就顶不住了得上双网卡或者万兆。CPU 上如果全程不转码只做 RTP 打包和转发单路占用的 CPU 非常低主要是内存拷贝和系统调用。我实测下来一台 8 核的机器跑 60 路不带转码CPU 平均占用在 20% 左右。但如果开了 FFmpeg 音频转码G.711A 转 AAC单路大概多占 3% 到 5% 的一个核60 路就需要额外的算力这时候建议用独立的转码节点别和接入节点混在一起。内存上每路要缓存 SPS/PPS、维护 RTP 序列号和时间戳加上 socket 缓冲区单路大约几十 KB 到几百 KB可以忽略不计。真正的内存大头是媒体服务器侧的 GOP 缓存ZLMediaKit 默认会缓存一定量的数据用于秒开路数多的时候要关注一下配置里的mergeWriteMS和缓存相关参数。6.3 日志、可观测性与安全边界日志要分级别别什么都打 INFO。我的做法是信令收发打 DEBUG状态转移打 INFO异常打 WARN 或 ERROR。生产环境开 INFO出问题临时开 DEBUG。日志文件一定要做轮转中间件这种东西一天能打几百兆不轮转很快就把磁盘写满。用 logrotate 或者直接用 systemd 的 journald 都行。可观测性上至少要暴露三个指标在线通道数、重连次数、推流字节数。这三个数字能覆盖 90% 的故障判断。在线数掉了说明设备侧有问题重连次数飙升说明网络抖推流字节数不增长说明码流断了。我一般用一个简单的 HTTP 接口把这几个数字吐出来接入现有的监控系统就行不需要上很重的方案。最后说说安全边界这部分很容易被忽视。第一中间件相关的端口RTSP、RTMP、HTTP API绝对不要直接暴露在公网需要外部访问就通过反向代理加鉴权或者走内网专线。第二媒体服务器的secret必须改掉默认值它相当于管理接口的密码。第三设备凭据不要硬编码用配置文件加权限控制配置文件权限设成 600。第四在下游平台侧做访问控制比如给每个用户分配带 Token 的拉流地址避免流地址被随意转发。这几条都是很基础的操作但在实际项目里我见过太多没做的出事的代价远比加这几行配置高。我自己在这个项目上前后迭代了三版第一版只追求能出画面第二版解决了重连和花屏第三版才把分层、日志和扩容做扎实。如果说有什么最值得分享的体会那就是协议层一定要先抓包再写代码别对着网上的片段硬凑。我在协议解析上花的时间有七成是在验证和修正别人的经验剩下三成才是真正的编码。另外就是尽量别自己造轮子媒体服务器、FFmpeg 这些成熟工具已经解决了大量你没想到的问题把精力放在协议适配和稳定性上收益高得多。
返回列表