新闻详情

新闻详情

首页 / 资讯中心 / 详情

海康GB28181设备PS流解包实战:从RTP到H.264提取全解析

发布时间:2026/9/9 4:39:07来源:尧图网络
海康GB28181设备PS流解包实战:从RTP到H.264提取全解析
简介面向数字电视及流媒体开发者这份代码围绕28181/DVB环境中的MPEG-2节目流解析需求解决PS流中视频、音频及附属数据难以直接读取的问题。资源以RAR压缩包交付共2个文件包含一个C头文件与一个实现文件体积仅3KB结构紧凑便于阅读与裁剪。目前已有852人学习下载。代码给出了完整解析器骨架头文件定义了解析类、对外接口与关键常量实现文件覆盖包头识别、PID过滤、PES包重组、PTS/DTS时间戳处理等核心步骤能够将TS/PS码流逐步拆解为可用的音视频数据。开发者既可以借此理解DVB与MPEG-2系统层的实际编码流程也能将其作为基础模块集成到机顶盒、播放器或检测工具中或按需扩展私有数据解析逻辑对于具备C/C基础、正在研究数字电视协议栈的工程师这是一份轻量且可以直接上手的参考实现。 做国标GB/T 28181对接的开发者基本都逃不过和海康Hikvision圈里习惯叫HK设备打交道。我第一次调28181的时候脑子里预演的是RTP里直接装H.264裸流收到包、拼帧、送解码器完事。结果抓包一看RTP的payload根本不是裸流而是一层带00 00 01 BA前缀的MPEG-2 PS数据当场就愣住了。平台侧FFmpeg的RTSP实现不会去解PS复用SIP信令都通了媒体却卡在这一层。这篇就把海康28181设备PS流解包这件事彻底讲清楚为什么海康非要给媒体包一层PSPS流的结构到底怎么排的怎么写代码把PS里的H.264安全地取出来以及我实际联调中踩过的那些坑。适合正在做国标平台、流媒体网关、录像存储服务的开发者参考尤其是第一次接海康设备、对着抓包文件找不到北的新手。1. 海康国标设备为什么偏要用PS封装1.1 GB28181对媒体传输的约定GB/T 28181标准里媒体传输走RTP/RTCP这点大家都没异议。但标准没把话完全说死视音频编码数据可以用PS封装也可以直接封装成H.264或H.265裸流。问题在于海康设备实际实现里无论是H.264还是H.265几乎默认走PS封装尤其是一些老固件型号。你配置好了SIP注册一路信令正常收到的媒体却是PS流这是常态不是异常。为什么标准层会选PS我个人的理解是PS封装是MPEG-2系统层标准天然支持多路视频、音频、字幕数据复用在一个流里还能带上PTS/DTS时间戳和码率信息。GB28181既要视频又要音频还要处理音视频同步直接复用现成的PS封装比裸流再自己造一套时间同步机制省事得多。对设备厂商来说这也是一个成熟稳定的选择芯片SDK里基本都有PS打包模块等于白送。对我们做接入的人来说这就意味着不管你想不想要海康国标流基本默认就是PS流。你不能指望设备给你发裸H.264必须自己承担PS解复用这层活。想跳过这层只能走平台侧主动取流时协商其他封装但大多数时候协商不下来PS就是最大公约数。1.2 抓包看到的海康PS流长什么样用Wireshark抓一次海康设备28181取流过滤RTP包看payload部分典型的PS流字节长这样00 00 01 BA 44 00 04 00 24 01 ... // PS Pack Header 00 00 01 BB ... // System Header可选 00 00 01 BC ... // Program Stream Map (PSM) 00 00 01 E0 00 00 ... // 视频PES包00 00 01 BA是PS包头的起始码相当于一个集装箱的箱头00 00 01 BC是节目流映射PSM相当于集装箱清单00 00 01 E0是视频PES包的起始码真正的H.264/H.265数据就装在这里面。一个RTP包通常装一个PS包但RTP分片时一个PS包也可能跨多个RTP包后文会详细说。看到这个结构第一反应可能就是直接拿FFmpeg解一下不行吗可以但很多嵌入式平台或自研网关里不方便直接引FFmpeg或者你只是需要一个轻量的解包模块不想要整个流媒体框架。这种情况下对PS流结构有清晰认识自己写一个解包函数比引一个重型依赖更可控。2. 解包前先搞懂PS流的四个层级2.1 Pack Header的定位与跳过公式PS流的最小单元叫PS包每个PS包以Pack Header开头。Pack Header以00 00 01 BA起始后面跟着一长串固定字段包括SCR系统时钟参考、program_mux_rate等。真正写代码时我们不需要完全读懂这些字段但必须能正确跳过它们否则解析指针就乱了。MPEG-2 Program Stream的Pack Header长度是一个可变值计算公式是固定10字节再加尾部填充字节长度。规则如下起始码00 00 01 BA占4字节接着是10字节固定字段其中最后一字节的低3位表示后面还有多少个填充字节所以Pack Header总长度 4 10 stuffing_length 14 stuffing_length。判断固定10字节的最后一字节偏移是在起始码之后第9个字节。如果你已经用指针p指向BA后面的第一个字节那么p[9] 0x07就是填充长度。不是所有设备都填充海康我实测下来很多包是不填充的也就是stuffing_length为0Pack Header直接就是14字节。但代码里不能写死必须动态跳。这里有个常见误区有些教程会让你直接搜索下一个00 00 01 BB或00 00 01 BC来定位PSM这在小样本抓包下能跑通。但一旦PS包里有别的二进制数据恰好凑出起始码或者系统头不存在就会跳错位置。老老实实按语法解析Pack Header长度才是稳妥做法。2.2 PSM和System Header的作用PS包在Pack Header之后通常跟着System Header起始码00 00 01 BB和PSM起始码00 00 01 BC。System Header用于描述整个节目流的基本属性比如最大码率、是否区分音频视频缓冲等海康流里它有时候有有时候没有做兼容时不能依赖它存在。PSM则是一张节目流映射表它列出了这个PS流里包含哪些基本流每个基本流的流类型和流ID是什么。解析PSM的结构是起始码4字节、接下来2字节表示PSM剩余长度、后面是节目流映射信息等一堆字段。对我们取视频流的场景PSM真正有用的信息就是告诉你视频流的stream_id是不是0xE0音频流是0xC0还是0xBD。不过实际工程里我很少去精细解析PSM因为大多数海康设备PSM内容固定直接扫描PES起始码效率更高四字节对齐后做分支判断就够了。但建议至少把PSM长度解析出来并跳过不要把它当普通数据混进PES否则会把H.264解析线程搞乱。2.3 PES包才是真正的数据罐头PS容器里真正装着音视频编码数据的是PES包。PES起始码格式是00 00 01 xx其中stream_id范围含义0xC0-0xDF音频流0xE0-0xEF视频流0xBDprivate_stream_1部分设备用它封音频或私有数据0xBC节目流映射PSM视频PES包的典型起始序列是00 00 01 E0。紧随其后是2字节的PES_packet_length它表示从这个字段之后到PES包结束还有多少字节。如果该值为0表示长度未知需要扫描下一个起始码来确定边界海康设备通常都会给具体长度但H.265流某些分片场景下我也遇到过填0的情况代码里要做兜底。PES_packet_length之后并不是编码数据而是一个可选的PES头里面包含了PTS/DTS时间戳等关键信息。PES头的长度通过一个字节的PES_header_data_length字段指定但这个长度不包含前3个字节标志字节和长度字节本身。所以ES数据的起始位置是ES起始偏移 6(起始码stream_idpacket_length) 3(PES头固定字段) PES_header_data_length ES数据长度 PES_packet_length - 3 - PES_header_data_length这句计算是整个解包代码的核心记住了它PS解包就成功了一半。3. 可直接落地的PS流解包代码3.1 C版核心解析函数如果你的业务是C后端核心解析函数可以写得很轻量。一个ParsePackHeader用来跳Pack Header一个ParsePes用来取视频PES里的ES数据和PTS。下面是我在项目里用的简化版注释已经标清楚了// p指向0x000001BA之后的第一个字节 // 返回Pack Header总长度解析失败返回-1 int ParsePackHeader(const uint8_t* p) { // MPEG-2 PS Pack Header固定字段第一字节高2位必须是01 if ((p[0] 6) ! 0x01) return -1; // p[9]是固定10字节的最后一字节低3位表示填充字节数 int stuffing_len p[9] 0x07; // 4字节起始码 10字节固定字段 stuffing_len return 14 stuffing_len; } // buf指向00 00 01 E0这样的视频PES起始码 // 解析成功后通过引用参数返回ES数据位置、长度和PTS bool ParseVideoPes(const uint8_t* buf, int buf_len, int es_start, int es_len, int64_t pts) { if (buf_len 9 5) return false; // buf[4]和buf[5]是PES_packet_length int pes_len (buf[4] 8) | buf[5]; if (pes_len 0) return false; // 长度未知需要按起始码扫描 // buf[6]是标志位buf[7]低2位是PTS_DTS_flags int pts_dts_flags (buf[7] 6) 0x03; if ((pts_dts_flags 0x02) ! 0) { // PTS通常从buf[9]开始共5字节 const uint8_t* b buf 9; pts ((int64_t)(b[0] 1) 0x07) 30 | ((int64_t)b[1] 22) | ((int64_t)(b[2] 1) 15) | ((int64_t)b[3] 7) | ((int64_t)(b[4] 1)); } // buf[8]是PES_header_data_length int header_data_len buf[8]; // 关键公式ES起点和长度 es_start 9 header_data_len; es_len pes_len - 3 - header_data_len; return es_start es_len buf_len; }实际在主循环里对每一段收到的PS数据先扫描00 00 01 BA、00 00 01 BC、00 00 01 E0这些起始码遇到Pack Header就按上面的公式跳过遇到PSM也解析长度跳过遇到视频PES就提取ES数据和PTS。这个状态机写起来不复杂但边界检查一定要做扎实尤其es_len为0或为负时必须跳过否则后面解析H.264直接内存越界。3.2 Python版快速验证脚本调试阶段用C编译太慢我习惯先用Python写一个验证脚本把抓包文件里的PS流直接拖进去跑一遍确认结构理解没错再翻译成C。下面这个脚本可以快速把PS流里的视频ES抽取出来def append_es_as_annexb(out: bytearray, es: bytes): 把ES数据追加到输出缓冲转成Annex-B格式。 如果ES里已经自带起始码就直接追加否则补一个起始码。 if es.startswith(b\x00\x00\x01) or es.startswith(b\x00\x00\x00\x01): out es else: out b\x00\x00\x00\x01 es def extract_h264_from_ps(data: bytes) - bytes: pos 0 n len(data) out bytearray() while pos 4 n: if data[pos:pos 4] b\x00\x00\x01\xba: # Pack Header stuffing data[pos 13] 0x07 pos 14 stuffing elif data[pos:pos 4] b\x00\x00\x01\xbc: # PSM psm_len int.from_bytes(data[pos 4:pos 6], big) pos 6 psm_len elif data[pos:pos 3] b\x00\x00\x01 and 0xE0 data[pos 3] 0xEF: # 视频PES pes_len int.from_bytes(data[pos 4:pos 6], big) if pes_len 0: pos 6 continue header_data_len data[pos 8] es_start pos 9 header_data_len es_len pes_len - 3 - header_data_len if es_start es_len n: break es data[es_start:es_start es_len] append_es_as_annexb(out, es) pos es_start es_len else: pos 1 return bytes(out)这个脚本验证结构足够了。注意它没有处理RTP分片组包真实取流时要先根据RTP头的sequence number和时间戳把属于同一个PS包的RTP payload拼完整再交给这个函数。3.3 解包后如何拼装H.264裸流从PES里拿到的ES数据未必就是干净的一个NALU可能是连续多个NALU拼在一起也可能一个NALU被截断成两个PES包。最省事的做法是把所有提取到的ES字节连续追加统一写成Annex-B格式即每个NALU前面保证有起始码再整体喂给解码器或者封装成MP4。上面Python脚本里append_es_as_annexb做了一个简单处理如果ES已经带起始码就直接追加没带就补一个00 00 00 01。但严格来说ES内部有多个NALU时每个NALU前都应该补起始码不然解码器可能只拿到一个超大块无法拆分。更严谨的写法是循环扫描ES内部的00 00 01或00 00 00 01按NALU粒度分别加起始码。不过实际联调中海康PES payload里通常自带起始码或者整个payload就是一个完整访问单元简单追加也能解出画面所以这个版本的容错能力其实够用。如果你要把解出来的H.264裸流再推给播放器建议用FFmpeg的AVBitStreamFilter里的h264_mp4toannexb它能把AVCC格式的H.264转成标准Annex-B比自己拼更可靠。但国标PS流的ES数据本来就是类似Annex-B的形态自己转一遍后交给播放器问题不大。4. 实测中最容易踩的坑4.1 SPS/PPS不常驻必须缓存参数集PS解包本身不难难的是解包之后能不能出画。H.264解码器在开始解码前必须先拿到SPS和PPS参数集。RTSP流里SPS/PPS通常通过SDP报文携带但GB28181的SIP信令里经常不给你PS流的PSM里也不保证带。海康设备通常在第一个关键帧的PES payload里把SPS/PPS和IDR帧一起下发。这就意味着你的解包模块不能只做一个简单的管道无脑输出ES数据。你要在解包过程中识别SPS和PPS的NALU类型保存下来在输出关键帧之前把它们拼到IDR帧前面。否则解码器初始化时拿不到参数集画面要么黑屏要么花屏。NALU类型判断是(nalu_type 0x1F) 7表示SPS(nalu_type 0x1F) 8表示PPS。做法是在I帧缓存参数集的逻辑里先抓取SPS/PPS保存到内存输出Annex-B流时在关键帧前优先输出缓存的SPS/PPS再输出IDR载荷。有些平台的解码器支持裸流自动搜索参数集但不要依赖自己在解包层做一次缓存最稳妥。4.2 RTP分片、PS包跨包与粘包海康国标流默认的RTP MTU配置下一个PS包往往拆成多个RTP包发送。解包前必须做RTP组包按照RTP头的sequence number判断是否连续同一个timestamp的多个RTP包拼成一个完整的PS包再交给PS解析逻辑。如果漏了这步你会发现解析器经常报错、丢帧。另一个问题是粘包一个RTP包可能包含多个PS包。尤其是大码率场景下设备可能把两个相邻PS包塞进同一个RTP payload。解析逻辑不能假设一个RTP包只装一个PS包必须用循环扫描起始码的方式处理完整个payload直到剩余字节不足4个才结束。判断数据边界的时候强烈建议以RTP的timestamp作为PS包的逻辑边界标识但PS包的实际字节边界要以00 00 01 BA起始码为准。组完包再解包顺序不能反。4.3 PTS和RTP时间戳别搞混RTP头里有一个时间戳PES头里也有PTS/DTS两个时间戳的时钟频率和参考基准可能都不一样。我以前图省事直接用RTP时间戳换算播放时间结果画面出帧正常但延迟和快慢越来越不对。后来换成PES里的PTS才稳定下来。PTS的时钟频率是90kHz即PTS值 / 90000就是秒。解包时从PES头解析出PTS后最好在帧数据里同时记录这个PTS交给下游播放器或录像模块时也统一用PTS作为时间基准。RTP时间戳只用于RTP层的排序和丢包重组不要混用。4.4 海康H.265设备的私有细节海康H.265编码的28181流视频PES的stream_id依然是0xE0这点没有特殊差异。但实际联调中有些老固件会把视频数据丢在0xBDprivate_stream_1里这其实是部分老设备的私有打包方式。解包逻辑里不能只匹配0xE0最好把0xBD也纳入扫描范围然后进一步通过对ES数据的NALU头判断是视频还是音频。另外国标PS流在一开始可能并没有PSM只有Pack Header和PES包。这时候解包器如果强制要求先见到PSM再开始解PES流就会被卡住。正确的姿态是PSM出现就解析不出现就直接扫描PES起始码两边都兼容。5. 联调工具与验证思路5.1 用FFmpeg坐实解包结果写完解包函数怎么验证它解出来的H.264是对的我常用的一个笨但有效的方法是把提取出来的裸流保存成.h264文件再用FFmpeg转成MP4看能不能正常打开ffmpeg -f h264 -i extracted.h264 -c copy out.mp4如果FFmpeg能正常输出视频帧说明解包逻辑没问题。如果报错比如no frame!或者花屏先别怀疑解码器回头检查你的PS包边界定位尤其Pack Header的跳过长度是否算错了。更省事的方式是直接用FFmpeg解PS流做交叉对照ffmpeg -i input.ps -c copy out.mp4。FFmpeg内部有成熟的MPEG2PS解复用器它能不能解析成功代表了这个PS流的合法性。你的代码解析结果和FFmpeg比哪一帧开始出现偏差就能快速缩小问题范围。5.2 日志打印和Hexdump习惯联调PS解包时日志比调试器好用。每收到一个RTP包打印sequence、timestamp、payload长度每解析到一个PS包打印pack header偏移和长度每提取到一个视频PES打印PES长度、ES长度、PTS。这样一旦解析链断裂看日志就能定位是哪一步跳错了。如果怀疑解析指针偏移出错直接把当前处理的一段PS数据用hexdump打印出来配合起始码的位置人工核对一遍。这个习惯帮我抓出过好几个隐蔽的字节偏移错误比如把PES起始码从第3个字节而不是第4个字节开始算当时排查了半天最后就是靠hexdump一眼看出来的。5.3 先跑通主流再谈兼容海康摄像头、NVR、平台SDK的PS流实现并非完全一致不同固件版本之间差异不小。不要指望一份代码从出生就兼容所有设备。我的建议是先把最常见的场景跑通也就是单路视频、MPEG-2 Pack Header、PSM存在、视频PES stream_id等0xE0这个组合确认画面稳定流畅之后再逐步加入对H.265、音频PES、老固件0xBD封装等分支。每加一个分支就用抓包数据回归一次。我一般会把各种设备的抓包文件存成测试样例接入代码升级后统一跑一遍看解包结果和预期是否一致。PS解包这种底层模块回归测试的价值极高改一个字节的偏移判断影响的可能是一整批设备。最后再分享一个小技巧国标PS流解包这件事看着复杂真正核心的代码其实不到200行。你不需要把MPEG-2系统层的每个字段都读懂只需要抓住Pack Header长度公式、PES长度计算、起始码扫描这三个支点剩下遇到的特殊情况都是见招拆招。先跑通一条流再慢慢打磨不要一上来就追求完美兼容所有海康设备。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

响应式设计进阶:从媒体查询到容器查询的现代布局体系 2026/9/9 5:21:09

响应式设计进阶:从媒体查询到容器查询的现代布局体系

我经常被问到同一个问题:响应式设计是不是就是在CSS里加几个媒体查询?说实话,我做了这么多年前端,早期也这么想,但后来发现这完全是本末倒置。媒体查询只是兜底手段,真正的响应式设计应该是让界面像水一样&…

阅读更多 →
程序员思维陷阱:从苏享茂事件看人生系统的风险防御 2026/9/9 5:21:09

程序员思维陷阱:从苏享茂事件看人生系统的风险防御

2017年9月7日凌晨,苏享茂从北京家中跳下,留下一条遗书和一段41天的短暂婚姻。那天早上,我所在的几个技术社群几乎同时被“WePhone创始人自杀”这条消息刷屏。作为同在这个行业里写代码、做产品的人,看着一个技术出身、做出过千万级…

阅读更多 →
原创图片被抄袭?用哈希指纹和感知哈希技术高效取证维权 2026/9/9 5:21:09

原创图片被抄袭?用哈希指纹和感知哈希技术高效取证维权

如果只是被口头道歉,图片却还在各平台继续传播,问题就远没有结束。对画图的人、做设计的开发者、做独立产品的团队来说,原创表情、插画、界面素材被临摹或直接抄走,其实不是一个小概率事件。更麻烦的是,道歉只能处理“…

阅读更多 →
工业搬运机器人PLC控制系统设计与调试实战 2026/9/9 5:21:09

工业搬运机器人PLC控制系统设计与调试实战

做工业搬运机器人这个方向,我是从一条完整的自动化装配线开始入坑的。那会儿甲方要我在三周内拿出一套双工位上下料方案,负载不大,只有8公斤,但节拍卡得死,动作路径又不能跟旁边的气动设备打架。后来设备落地稳定跑了一…

阅读更多 →
Comsol 6.0流体对电弧影响仿真:从多物理场耦合到参数扫描实践 2026/9/9 5:21:09

Comsol 6.0流体对电弧影响仿真:从多物理场耦合到参数扫描实践

做开关电器和放电加工方向这么久,我一直有个很深的体会:电弧这个看起来"纯电气"的东西,实际行为有一大半是由周围的流体决定的。你这边放个电,那边气体一吹,电弧形态、温度分布、甚至会不会熄灭,…

阅读更多 →
JavaSE复习指南:从语法基础到JVM的核心知识地图 2026/9/9 5:18:09

JavaSE复习指南:从语法基础到JVM的核心知识地图

1. JavaSE知识复习,先画出属于你的技术地图JavaSE这个名称,大学课本里出现过无数次,但真到了找工作、写项目、回炉复习的时候,大家才发现它根本不是一门“随便背背就能过”的课。很多人在刷题阶段总会遇到一种奇怪的感觉&#xff…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞