首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
海康威视GB/T 28181 PS流解包实战:从RTP到H.264完整解析
📅 2026/9/9 4:35:45
✍️ 爱科研究院
👁 阅读 3,247
简介面向数字电视与流媒体开发者的28181 PS流解包代码定位于标准节目流的解析环节主要解决多路节目流中视频、音频数据的提取与重组问题。压缩包内共两个文件一个C源文件与一个头文件整体仅3KB代码精简、无冗余依赖适合快速移植或作为教学范例。解析过程覆盖了节目流头识别、包标识过滤、打包基本流重建以及显示时间戳和编码时间戳同步等关键流程开发者通过阅读解析器接口与实现逻辑可以掌握解包的典型写法理解多媒体数据在传输流中如何被封装和还原也能据此扩展出满足实际需求的信号解析模块方便在数字电视接收设备或相关软件中复用。目前已有852人学习下载对于正在研究数字视频广播标准或需要调试信号抓包信息的工程师颇具借鉴价值。1. 项目拆解HK 28181的取流链路里解包这步卡了多少人先说清楚这个标题里三个词到底是什么意思因为很多人一上来就翻代码结果连自己要解什么东西都没搞明白。HK指的是海康威视设备28181是GB/T 28181安防行业里做视频联网对接逃不开的一个国家标准。这个标准规定了摄像机、NVR、平台之间怎么用SIP协议做信令控制以及怎么用RTP把实时视频流从设备端推到平台端。而RTP包里装的不是裸的H.264而是PS流也就是MPEG-2标准里定义的节目流封装。所以整个取流链路大概是这样的平台向设备发SIP INVITE请求取流设备回200 OK并通过INVITE的SDP把媒体参数告诉平台紧接着设备开始往平台指定的IP和端口推RTP包RTP payload里面就是一层套一层的PS流最终真正的视频编码数据藏在PES包里。这个项目要做的事情就是把这一层层拆开从RTP payload里找到PS包头再跳过各种系统级信息定位到PES包最后从PES的payload里拿出完整的H.264/H.265编码数据才能送进解码器显示或者转封装。有人可能会问设备明明也支持RTSP为什么不直接拉RTSP答案很简单在28181的组网场景里设备要穿越复杂的SIP信令体系由上级平台统一管理而RTSP是点对点的简单拉流协议没有信令协商、目录管理、状态上报这一整套东西。所以国标接入必须走28181也就必须面对PS流解析这道坎。这个项目适合谁参考三类人一是做安防平台接入开发的二是做视频网关或者流媒体服务需要兼容国标设备的三是纯粹想搞懂PS封装协议、想找一份能跑通的解包代码的人。下面我把从协议结构到代码实现再到现场排障的完整过程拆开讲。2. PS流的包结构编码器输出的数据长什么样写解包代码之前必须先把PS流的包结构刻在脑子里。PS流就是一段连续的数据里面按顺序排列着不同类型的“包”每个包都有一个固定格式的起始码。解包的本质就是在这个二进制流里找起始码、分析包类型然后按类型跳过或提取数据。2.1 四个起始码四条分界线PS流里最常见的起始码有四个只要看到以下这几种就知道当前位置是什么包起始码Hex包类型作用00 00 01 BAPS包头部包含SCR时间基准、复用速率等00 00 01 BB系统头部描述流中的音视频流基本参数00 00 01 BCPSM节目流映射描述当前节目包含哪些基本流流类型和流ID的对应关系00 00 01 E0~EF视频PES包承载H.264/H.265视频数据00 00 01 C0~DF音频PES包承载G.711/G.722等音频数据00 00 01 BD私有流1常见于承载AAC音频需要特殊解析实际操作中海康设备推过来的PS流结构通常是这样一个PS头紧跟着一个系统头再来一个PSM然后是若干PES包视频PES和音频PES交替出现。也有部分设备不固定输出系统头和PSM或者PSM只在关键帧附近出现这时候解析代码就要做容错不能因为找不到某个包就罢工。2.2 PES头部的关键字段PES包是PS流的核心因为真正要的音视频数据都在PES的payload里。PES包的起始码是00 00 01加上8bit的stream_idstream_id决定了这个包是视频还是音频也决定了payload的格式。PES头关键字段主要是这几个stream_id8bit比如0xE0表示视频0xC0表示音频PES_packet_length16bit表示这个PES包从下一个字节开始到payload结束的总长度标志位第7、8字节包含PTS_DTS_flags等重要标记header_data_length第9字节表示可选头部数据的字节数PTS/DTS33bit的时间戳单位是90000Hz也就是1秒等于90000个时间单位不要小看PES头那些标志位PTS_DTS_flags是3bit的字段值等于2表示后面有PTS等于3表示PTS和DTS都有。解析的时候如果无视这个字段直接把后面的buffer当成payload去提h264数据必然会出现花屏、卡顿甚至完全解不出画面。3. 解包代码落地从RTP payload到干净的H.264流结构看懂了代码就好写了。下面给一个能用的解包框架C语言风格核心逻辑可以移植到C、Java、Go等任何语言。3.1 整体思路不要一次解析到底PS流解析最容易犯的错误是想在一个函数里把PS头、系统头、PSM、PES头、H.264 NALU全部处理完。实际上解包稳定的项目都采用状态机思路每一级解析只做自己的事第一层从RTP payload里提取PS流数据注意处理RTP扩展头和CSRC列表。 第二层循环扫描PS流识别包类型跳过PS头、系统头、PSM。 第三层定位到PES包后解析PES头拿到payload的长度和PTS/DTS。 第四层把PES payload里的ES数据提取出来按NALU起始码切分H.264数据。这样分层的好处是每一层职责单一出问题的时候能快速定位是在RTP解析、PS层解析还是PES层解析。3.2 找起始码的函数PS流里所有包都以00 00 01开头H.264的NALU在PES payload里通常以00 00 00 01或者00 00 01开头。所以解包的第一步是写一个可靠的起始码扫描函数static int find_start_code(const uint8_t *data, int size) { int i 0; for (i 0; i size - 4; i) { if (data[i] 0x00 data[i1] 0x00 (data[i2] 0x01 || (data[i2] 0x00 data[i3] 0x01))) { return i; } } return -1; }这里同时兼容了三字节起始码和四字节起始码。PS包本身用三字节起始码就够但H.264裸流常以4字节起始码切分NALU。实测中不少海康设备在PES payload里输出的是00 00 00 01所以两个都要认。3.3 解析PS包与PES头拿到起始码位置后根据起始码后面的一个字节判断包类型。PS头0xBA的结构比较固定一般跳过固定长度就能到下一个包。注意PS头里的SCR字段是45bit分布在多个字节的分散位上如果不是要做精确的时钟同步可以直接按固定长度跳过只需要保证长度正确。PES头的解析则需要逐字段读下面是一个标准的解析流程typedef struct { int stream_id; int pes_packet_length; int has_pts; int has_dts; int header_data_length; uint64_t pts; uint64_t dts; } PESHeader; int parse_pes_header(const uint8_t *buf, int buf_size, PESHeader *pes) { int offset 0; // 跳过起始码 00 00 01buf[3]是 stream_id if (buf_size 9) return -1; pes-stream_id buf[3]; pes-pes_packet_length (buf[4] 8) | buf[5]; // 第6字节是 10 PTS_DTS_flags(2bit) ESCR/ES_rate等标记 // 第7字节是其他标志位 // 第8字节是 header_data_length pes-header_data_length buf[8]; int flags (buf[6] 6) 0x03; // PTS_DTS_flags pes-has_pts (flags 0x02) ! 0; pes-has_dts (flags 0x01) ! 0; offset 9; if (pes-has_pts || pes-has_dts) { // 有PTS或DTS时先读5字节的PTS if (buf_size offset 5) return -1; uint64_t pts 0; pts | ((uint64_t)(buf[offset 0] 0x0E)) 29; pts | ((uint64_t)buf[offset 1]) 22; pts | ((uint64_t)(buf[offset 2] 0xFE)) 14; pts | ((uint64_t)buf[offset 3]) 7; pts | ((uint64_t)(buf[offset 4] 1)); pes-pts pts; offset 5; // 有DTS时PTS后面再跟5字节DTS if (pes-has_dts) { if (buf_size offset 5) return -1; uint64_t dts 0; dts | ((uint64_t)(buf[offset 0] 0x0E)) 29; dts | ((uint64_t)buf[offset 1]) 22; dts | ((uint64_t)(buf[offset 2] 0xFE)) 14; dts | ((uint64_t)buf[offset 3]) 7; dts | ((uint64_t)(buf[offset 4] 1)); pes-dts dts; offset 5; } } // 跳过PES头里的可选字段到达payload起始位置 if (pes-header_data_length (offset - 9)) return -1; offset (pes-header_data_length - (offset - 9)); return offset; // 返回payload偏移 }这段代码里的位运算就是从MPEG-2标准里的分散位把33bit的PTS/DTS拼出来。PTS的单位是90000Hz用它计算显示时间时除以90000就得到秒。我见过有人把PTS当普通整数直接除以1000用结果越到后面音画不同步越明显这是单位搞错了。3.4 视频载荷提取与NALU组装拿到PES头部的payload偏移后从payload里提取H.264数据就轻松了。PES payload就是一段ES流这里面连续存放着NALUNALU以00 00 00 01或00 00 01开头。只需要按起始码把NALU切出来按需保留或丢弃。实际操作中不要每个NALU单独处理最好是把一个PES包里的ES数据整理成完整的数据块再统一分析NALU类型。因为H.264的SPS和PPS通常在视频流的最开始或者出现在关键帧附近如果设备配置变化SPS/PPS也会中途更新。做播放器接流的时候SPS/PPS必须单独缓存下来在关键帧之前送给解码器否则IDR帧到了也解不动。代码层面我的做法是设置一个标记第一次解析到0x67SPS和0x68PPS时把整段数据复制到单独的buffer里后面每次需要喂解码器配置信息时直接复用。这一步很多人忽略结果代码逻辑看着没问题画面就是出不来。4. 现场排障实录海康设备PS流的几个经典坑协议文档翻得再熟到了现场总会出现文档里没写的情况。下面这几个坑都是我在实际对接HIK设备时踩过的写出来供参考。4.1 一个RTP包装着多个PS包最开始我的解析逻辑是RTP payload的第一个起始码一定是PS包起始码解析完第一个PS包就认为这个RTP包处理完了。结果用某型号老款IPC测试时画面时不时卡一下抓包分析发现设备有时会把两个完整的PS包缝在同一个RTP payload里后面那包数据就整个丢了。解决办法是在解析完一个PS包后检查当前解析位置到RTP payload末尾的剩余数据是否还有足够长度再循环一次起始码扫描而不是只处理一次就退出。4.2 一个PS包里有多个PES包还有一种情况一个PS包里既有视频PES又有音频PES。特别是开了音频功能的设备视频PES和音频PES会交替出现。解析时如果只提取了第一个视频PES就认为当包处理完音频数据就会全部丢掉音画不同步问题就是这样产生的。正确的循环逻辑是遇到0xE0流ID就把payload交给视频处理流程遇到0xC0流ID就交给音频处理流程全部PES处理完之后同一RTP包循环结束。4.3 私有流1里藏着AAC海康设备开启AAC音频后音频PES的stream_id不一定是0xC0而常常是0xBD也就是私有流1。私有流1的payload结构比标准音频PES复杂常见的是ADTS头加AAC裸流有的设备还带PDposition descriptor信息。处理私有流1时不能直接把payload当PES payload用。正确的做法是先解析PD信息获取实际音频数据子流的偏移量再从偏移位置开始解析ADTS头最终拿到AAC帧。如果只是做简单转码不关心延迟也可以直接从ADTS同步字0xFFF开始扫描找到ADTS头再解析帧长。4.4 PTS/DTS时间戳溢出与复位28181设备PTS基准是90000Hz以32位有符号数来看大概24小时后就会溢出。实际测试中设备重启后PTS会回零。如果播放器直接把PTS当绝对时间用就会出现跳变甚至画面卡住。处理方案是在解包层把PTS统一转成相对时间戳以第一帧的PTS为基准做差值计算。同时用int64保存差值避免计算过程中溢出。对音视频同步来说用相对时间戳就够了不需要关心绝对时间原点。4.5 排查工具与调试顺序遇到问题别对着代码干瞪眼工具用起来抓包工具首选Wireshark过滤rtp后可以直接看RTP payload的16进制内容确认是不是以00 00 01 BA开头这一步能快速判断PS流是否正常到达。验证裸流是否正确可以用ffprobe或者ffplay打开RTP流。如果ffplay能出画面说明PS封装基本没问题问题大概率在代码解析层。如果ffplay也出不来就得回到抓包看封包结构。我自己调试时还有个习惯写一个dump工具把RTP payload保存成文件再用16进制编辑器看起始码分布。看到连续多个00 00 01 BA在一个payload里就能立刻想到是多PS包拼接的问题不用猜。5. 最后再分享几个排障之外的细节解包代码写得再完美也只是28181接入链路中间的一环。几次项目下来我最大的体会是不要等到联调阶段才开始写解包代码提前准备一个可以离线解析PS流文件的测试工具能省掉非常多现场联调的痛苦。具体来说先用VLC或者ffmpeg从设备拉一段RTP裸流存成文件然后在办公室里慢慢解析。反复用同一段文件检验修改后的代码效果比在项目现场对着摄像头调试效率高十倍。这个方法同样适用于排查设备型号差异导致的问题可以保留每个设备型号的抓包文件形成一个小的样本库。另外建议把解包层和上层业务解耦。解包代码只负责输出“一帧一帧的H.264数据块加时间戳”不要在里面塞播放、录制、转发的业务逻辑。我之前一个项目把音频解包和视频解包写进了同一个回调函数后期加音频对讲功能时不得不大规模重构。把解包层做成纯粹的中间件上层想喂给解码器、转封装成MP4还是转发给其他协议网关都是上层的事这才符合实际项目的演进节奏。PS流解包不是特别复杂的事但它考验的是对协议的耐心和对现场问题的敏感度。希望这篇东西能让正在跟28181流较劲的人少走几条弯路。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/9 4:35:45
拆解‘opencode’幻影:开发者认知错位与真实工具链重建
2026/9/9 4:35:45
OTP与2FA原理详解:从HOTP/TOTP算法到pyotp落地实践
2026/9/9 4:30:45
AI PPT工具可编辑性测评:从Gamma到python-pptx的避坑指南
2026/9/9 6:30:51
Spring Boot短信服务系统设计与实现:从验证码到前后端分离全解析
2026/9/9 6:30:51
opencode实战指南:终端AI编程助手的安装、配置与踩坑全记录
2026/9/9 6:30:51
VSCode雅蓝主题:安装定制与护眼配色实战指南
2026/9/9 6:30:51
Spring Boot+元宇宙:消费扶贫专柜管理系统设计与实现
2026/9/9 6:30:51
Qt HTTPS 通信指南:QNetworkAccessManager 与 TLS 证书避坑实践
2026/9/9 6:25:50
行政考勤统计提效:影刀RPA自动汇总考勤与生成报表实战方案
2026/9/9 0:00:26
MHS模型硬件标准:让大模型像调用软件一样控制物理设备
2026/9/9 0:00:27
AI五大核心方向详解:从机器学习到大模型,零基础转行选哪条?
2026/9/9 0:00:27
从50行最小循环到生产级AI引擎:工程化改造全解析
2026/9/9 2:07:00
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/9 1:41:51
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/9 5:25:52
基于CNN的调制信号识别:MATLAB实现时频图分类实战