简介:海康、国标PS流解析是音视频流媒体开发中常见的需求,压缩包内提供了一套可直接运行的工程源码,分为基于ffmpeg解析和直接解析PS流两个版本,方便不同层次的开发者选择学习。资源面向具备C/C++基础、希望掌握PS流解封装原理或快速集成国标流解析功能的工程师,既能通过ffmpeg版本快速实现音视频帧提取,也能通过不依赖第三方库的直接解析版本深入理解MPEG-2 PS流的结构、PTS/DTS及数据包分离等细节。压缩包共27个文件,以cpp源文件和h头文件为主,包含sln、vcxproj工程文件,可直接用Visual Studio打开编译,另有sdf、pdb等编译过程文件,整体体积仅2.24MB。内容预览显示目录清晰区分了ffmpeg版和直接解析版,并附有PsAnaly.hh和test_PsAnaly.cpp等关键文件,便于边阅读源码边对照验证。已有1095人学习下载,适合需要研究PS流解析细节或进行二次开发的音视频开发者。 干了几年安防平台开发,跟海康设备打交道是最多的。最近又把一份“海康、国标ps流解析”的资料翻出来整理了一遍,发现里面踩过的坑、总结出的经验,网上能系统讲清楚的真不多。很多朋友卡在“SIP信令已经通了、Invite也发出去了、摄像头也返回200 OK了,但拿到RTP包之后完全不知道从哪下手”。
这篇文章就围绕国标GB/T 28181协议里最核心也最让人头疼的PS流解析,把完整链路、报文结构、代码实现到排障思路全部讲透。无论你是做第三方平台接入海康设备,还是在搞自己的流媒体网关,只要涉及“海康”“国标”“PS流解析”,这篇文章都值得你花十分钟看完。
1. 国标平台接入的基本链路——从SIP信令到PS流
先理清楚一个基本概念:GB/T 28181不是单纯的推流协议,它是一套“信令+媒体”的组合。信令走SIP,媒体走RTP,而RTP里封装的内容就是PS流。很多刚入行的朋友会把H.264裸流和PS流搞混,以为摄像头推过来的RTP包直接拆出来就是视频帧,实际操作起来根本不是这么回事。
1.1 SIP信令交互与媒体协商过程
整个流程大概是这样的:平台作为SIP客户端,向海康设备的SIP服务器发起注册;注册成功之后,平台发送INVITE请求,请求里携带SDP信息,描述希望接收的媒体格式;设备响应200 OK,返回自己的SDP;平台再发ACK确认,然后设备就开始往平台指定的IP和端口推RTP流。
这里关键点在SDP协商阶段。海康设备在SDP的media描述里,视频编码通常标注为PS,而不是H.264。也就是说,设备明确告诉你“我推的是PS流”。如果你忽略这个标志,直接按H.264 RTP去解包,第一步就废了。
还有一个细节:国标规范里规定视频默认走PS封装,音频可以是G.711A或G.711U,而且音频和视频会打包在同一个PS流里通过同一路RTP传输。这就意味着解析PS流时不能只解视频,还得把音频一起处理。
1.2 为什么国标选择PS流而非TS流
接触过广电级开发的朋友会问:为什么不用TS流?TS流抗丢包能力强、支持切片,这个没错,但TS流的开销更大,而且国标在设计时更多考虑的是实时监控场景的简洁性。PS流结构更紧凑,适合在可靠或半可靠的传输环境中使用,解码端拿到完整PS包后解析也比较直接。
另外一个现实原因是历史兼容性。国内的视频监控厂家,包括海康、大华,早期做嵌入式设备时,处理PS流的代码库比TS流更成熟,沿用PS封装可以最大化复用原有代码,降低设备端改造成本。所以国标选PS流是一个综合考虑,不只是技术层面的选择。
1.3 拿到RTP流之后的第一步不是解PS,而是去RTP头
很多人栽在这上面。RTP包到达你的服务器端口后,你要先从UDP载荷里剥离RTP头,然后才是PS数据。RTP头有固定12字节,但如果开启了CSRC、扩展头,还要额外跳过对应长度。
我记得第一次调海康设备时,因为RTP扩展头没处理,导致PS流的起始码一直对不齐,调试了整整一个下午。后来抓包对比才发现,海康在RTP头里带了一个扩展字段,长度是4字节。所以解析时不能写死跳过12字节,一定要动态读RTP头里的扩展位和CSRC计数。
注意:正确的RTP头长度计算公式是
12 + 4 * CC(CSRC计数) + (X ? 4 : 0),其中X是扩展位标志。海康的部分型号开启扩展位,不处理就等着数据错位吧。
2. PS流结构拆解——别被一堆0x000001吓到
PS流全称是Program Stream,属于MPEG-2系统层封装。它的基本单位是PES包,PES包外面再套PS头、系统头、节目流映射表(PSM)。你可能要问:既然基本单位是PES,为什么还要额外加这么多头?
这就是H.264/H.265这种现代编码和MPEG-2系统层之间的“代沟”问题。PS流设计之初是给MPEG-2视频用的,H.264/H.265的NALU结构需要由PSM里的流类型来标识,再由PES头里的字段来对齐边界。所以解PS流不是简单找PES,而是要按固定顺序去识别人工构建的容器结构。
2.1 PS流的三层结构:PS头、PSM、PES包
一个完整的PS流长这样,按顺序排列:
- PS头:以
00 00 01 BA起始,里面携带系统时钟基准(SCR)和复用信息; - PSM:以
00 00 01 BC起始,描述这个PS流里面有哪些基本流(视频流类型、音频流类型),以及对应的流ID; - PES包:以
00 00 01 E0起始的是视频PES,以00 00 01 C0起始的通常是音频PES。
PS头和PSM并不是每个PES前面都有。实际上海康设备是“一个视频帧一个PES”,但PS头和PSM是间隔若干个PES才出现一次。这里必须注意:解析器不能假设每个视频PES前必然有PSM,否则遇到没有PSM的包就会崩。
2.2 手动解析一段PS流的二进制布局
我来展示一个实际的PS流字节序列,解释每个字段的含义。网上很多资料只讲理论,直接对二进制就懵了,这里一步步过:
00 00 01 BA 44 59 02 04 01 10 09 10 00 00 01 BC 00 0E ...00 00 01 BA:PS起始码,看到这个就知道PS头来了;44:后面这几个字节是SCR字段,精确到27MHz时钟,当场不用细抠,关键是知道它存在;00 00 01 BC:PSM起始码,紧接着的00 0E是这个PSM的长度,14字节;- PSM里会包含视频流ID(通常是
E0)和音频流ID(通常是C0),还有对应的编码类型描述符。
继续往下走:
00 00 01 E0 00 30 86 80 05 21 00 01 00 01 65 B8 04 00 00 01 09 F0 ...00 00 01 E0:视频PES起始码;00 30:PES包长度,48字节,包括PES头和数据;86 80:PES头标志,86表示MPEG-2版本,80表示这后面有PTS(显示时间戳);05 21 00 01 00 01:PTS的编码值,需要做位运算还原成真正的90kHz时间戳;65 B8:H.264 NALU类型为5,也就是IDR关键帧的起始码。
不对齐的PES包特别容易混淆,但只要你从00 00 01这三字节起始码出发,每次按长度字段跳转,基本不会迷路。PS流最核心的解析方法就是“找起始码+读长度+按长度跳转”,就这么简单。
2.3 PS流中H.264/H.265包含关系
H.264在PS流里不是一个NALU一个PES,而是一个视频帧一个PES,里面可能包含多个NALU。比如一个IDR帧的PES里,通常包含SPS、PPS、SEI和IDR Slice。所以你提取H.264裸流时,要把同一个PES里的所有NALU都拆出来,按顺序写入文件。
H.265也是一样,VPS、SPS、PPS、IDR都在同一个PES里。区别是H.265的NALU头是2字节,H.264是1字节。解析时要根据PSM里声明的编码格式去选择对应的NALU头长度,不然SPS和PPS边界都分不清。
以前遇到一个项目,从PS流提取H.265裸流,播放器一直打不开。排查到最后发现是VPS丢失了。H.265没有VPS,很多解码器直接罢工。所以提取裸流时,VPS、SPS、PPS都不能丢。
3. PS流解析实操——提取H.264/H.265裸流的核心代码
这一节直接给可运行的解析方案。核心逻辑不限语言,我用Python描述,方便你理解流程;生产环境用C++或Go改写,原理一致。
3.1 定义解析状态机
解析PS流不推荐一次性把所有数据读进内存再解析,单帧PS包动辄几十KB,一路码流持续几个小时,内存根本扛不住。建议用流式状态机,依次识别起始码,然后进入对应的解析分支。
class PsParser: def __init__(self): self.state = 'SEARCH' self.buffer = b'' self.video_frames = [] self.audio_frames = [] self.stream_id = None def feed(self, data: bytes): self.buffer += data while True: if self.state == 'SEARCH': # 查找起始码 idx = self.buffer.find(b'\x00\x00\x01') if idx == -1: # 没找到,保留末尾两个字节,防止起始码跨包 self.buffer = self.buffer[-2:] return if idx > 0: self.buffer = self.buffer[idx:] code = self.buffer[3] if code == 0xBA: self.state = 'PS_HEADER' elif code == 0xBC: self.state = 'PSM' elif code in (0xE0, 0xC0): self.state = 'PES' else: # 未知起始码或填充数据,跳过 self.buffer = self.buffer[4:] # 进入对应状态后继续处理...这个状态机的核心思想是:一次只解析一个包,解析完回到SEARCH,再找下一个起始码。这样不会有内存暴涨的问题,也方便做丢包恢复。
3.2 完整解析一个PES包
拿到PES起始码后,先读2字节的PES长度,再读PES头。PES头里最需要注意的是PTS,因为后续封装输出时需要它做时间同步。
def parse_pes(self, pes_payload: bytes): # pes_payload 不包含起始码 00 00 01 E0 pes_len = (pes_payload[0] << 8) | pes_payload[1] # 第3字节是标志位,第4字节低6位是头数据长度 header_data_len = pes_payload[3] & 0x3F pes_header_end = 6 + header_data_len if self.stream_id == 0xE0: # 视频 nal_units = self.extract_nal_units(pes_payload[pes_header_end:]) self.video_frames.append(nal_units) elif self.stream_id == 0xC0: # 音频 self.audio_frames.append(pes_payload[pes_header_end:])这里有一个容易犯的错:pes_len是整个PES包的长度,包含PES头和数据,不包含起始码。很多人按这个长度跳转时,跳多了或跳少了,后续所有位置全部错位。正确跳转方式是:读取完起始码和长度字段后,后续还有pes_len - 3个字节属于这个PES。
3.3 从PES载荷中提取NALU
视频PES的载荷是带起始码的NALU序列,直接按00 00 01切分就行。H.264的一个帧通常由多个NALU组成,全都要保留。
def extract_nal_units(self, payload: bytes): nals = [] start = 0 while start < len(payload): # 找起始码 idx = payload.find(b'\x00\x00\x01', start) if idx == -1: break # 找下一个起始码,确定本NALU结束位置 next_idx = payload.find(b'\x00\x00\x01', idx + 3) if next_idx == -1: next_idx = len(payload) nals.append(payload[idx:next_idx]) start = next_idx return nals切出来的NALU,注意前4个字节是起始码,写入裸流文件时要转成4字节长度前缀的AVCC格式,或者保留起始码的Annex-B格式,取决于你的解码器要求。大多数播放器两种都支持,但比如FFmpeg的h264_mp4toannexb滤镜是处理反向转换的,如果你给他AVCC格式反而会报错。所以这里最好根据用途灵活处理。
3.4 时间戳恢复与音视频同步
PS流的PTS是33位,用90kHz时钟,需要从编码字节恢复。PTS在PES头里的存储是分散的,不能直接读,要做位组装:
def parse_pts(data: bytes) -> int: # data是包含PTS的5字节区域 pts = 0 pts |= (data[0] >> 1) & 0x07 pts <<= 15 pts |= (data[1] >> 1) & 0x7F pts <<= 15 pts |= (data[2] >> 1) & 0x7F pts <<= 15 pts |= (data[3] >> 1) & 0x7F return pts拿到视频和音频的PTS后,做同步就简单了:视频PTS和音频PTS差值在正负一个帧间隔内,就认为是同步的。海康设备默认视频帧率25fps,帧间隔40ms,也就是3600个90kHz ticks。差值超过这个范围就需要做延迟补偿。
注意:有些海康设备的音频PTS并不准确,尤其当音频编码为G.711A时,PTS粒度比较粗。遇到音画不同步,先把音频缓冲200ms再播放,大部分情况能解决。之后再通过统计平均延迟做动态调整。
4. 常见问题与排查技巧实录
这部分是踩坑最多的内容。我在调试海康国标PS流时遇到的问题,全部整理成速查表,每个问题都对应一个实际案例,方便你对照排查。
4.1 第一帧黑屏或花屏,不是解码器的问题
现象:播放器起播后黑屏几秒才开始出画,或者直接花屏。
排查方向:
- 检查是不是没有等待IDR帧。PS流解析器如果从非关键帧开始解析,解码器会持续报错,直到等到下一个IDR。所以起播时应该丢弃非关键帧数据,直到遇到第一个IDR帧再开始送解码器。
- 检查RTP头长度是否处理正确。海康某些型号开启了RTP扩展头,长度判断错误会导致PS起始码错位,表现为随机花屏、卡死。
- 检查PSM里的流类型和实际编码是否一致。SDP协商为H.264,但设备实际推的可能是MPEG4,这种“表里不一”的情况在海康老型号上出现过,解析前最好做一次类型探测。
4.2 音频有杂音或无声,G.711A的坑
现象:画面正常,但音频是刺耳的杂音,或者完全没声音。
排查方向:
- 确认PSM里音频流ID是
C0还是C1。有些设备把音频流ID设置成C1,如果你的解析器只认C0,音频直接被丢掉了。 - 确认解码器参数。G.711A是8kHz采样、8bit编码,很多播放器默认按16bit PCM处理,就会出现严重杂音。需要先做G.711A到PCM的转换,再做音频参数协商。
- 音频RTP的时间戳单位是8kHz,不是90kHz。如果按视频时间戳逻辑处理音频RTP时间戳,会直接乱套。
4.3 RTP分片与重组,UDP丢包怎么处理
现象:网络波动时,画面频繁卡顿,甚至整个PS流解析中断。
排查方向:
- 海康设备默认MTU是1500,如果PS帧大于MTU,会做RTP分片。每个分片的RTP时间戳相同,需要按序列号重组后再解析。
- UDP丢包会导致PS流起始码错位。解析器要有“重新同步”机制,比如连续解析失败时,丢弃缓冲数据,重新搜索
00 00 01 BA。没有同步机制的解析器,一旦丢包就彻底卡死,必须重启拉流。
| 常见问题 | 典型原因 | 处理方式 |
|---|---|---|
| 花屏/黑屏 | 未等IDR帧、RTP头长度错误 | 丢弃非关键帧直到IDR,动态计算RTP头 |
| 音画不同步 | PTS精度不一致 | 音频缓冲200ms,统计动态调整 |
| 有画面无声音 | 流ID错误或G.711A未转换 | 识别C1流ID,做G.711A转PCM |
| 解析中断 | UDP丢包导致起始码错位 | 增加重新同步机制 |
| 视频绿屏 | H.265解成H.264 | 检查PSM流类型,动态切换解码器 |
4.4 抓包与调试工具
调试PS流最有效的工具还是Wireshark。抓包时要过滤RTP端口,右键解码为RTP,然后看RTP载荷的起始字节。如果载荷以00 00 01 BA开头,说明PS流对齐正常。如果载荷开头是乱的,说明RTP头长度判断有问题。
Wireshark里还能直接看RTP的CSRC计数和扩展位,帮你确认头长度。调试阶段建议用这个流程:
- 先抓包10秒,过滤出RTP流;
- 看RTP头的前12字节,确认SSRC、序列号、时间戳;
- 看载荷前4字节,确认是不是
00 00 01 BA; - 如果不是,调整RTP头长度,继续抓包验证。
我在实际项目中常用一个技巧:把RTP载荷导出来,单独存成.ps文件,用FFmpeg直接解析。
ffprobe dump.ps ffmpeg -i dump.ps -c:v copy video.h264如果FFmpeg能正确解析,说明PS流数据本身是好的,问题出在你的解析器;如果FFmpeg也报错,说明抓包阶段就有问题,优先检查RTP头。
5. 从PS裸流到播放器的最后一公里
解析出H.264/H.265裸流后,后续怎么处理取决于你的应用场景。这里整理几种常见方案,方便你根据自己的架构选型。
5.1 对接FFmpeg实现转封装与转码
用FFmpeg的libavformat库,可以直接把PS流封装成FLV或MP4,不需要自己写解析逻辑。但对于低延迟场景,不推荐直接丢给FFmpeg做转封装,因为FFmpeg的PS demuxer是按文件设计的,延迟较大。
比较稳的做法是:自己解析PS流,提取裸流后,通过FFmpeg的av_write_frame接口直接写FLV或RTMP。这样既能利用FFmpeg的编码能力,又能保持低延迟。
5.2 国标平台中PS流到WebRTC的低延迟链路
如果你想在浏览器里看海康国标摄像头,完整链路是:PS流解析→H.264/H.265裸流→封装为RTP→推给WebRTC网关。这里最难的不是PS解析,而是时间戳映射。PS流的PTS是90kHz,WebRTC要求RTP时间戳也是90kHz,所以可以做一个线性映射,但要注意回绕问题。
回绕是很容易忽略的坑:33位PTS大约26.5小时回绕一次,不做回绕检测的话,播放超过一天后时间戳会突然变成负数,导致画面冻结。处理方式是比较相邻PTS差值,如果差值为负且超过0x10000000,就认为是回绕,加上2^33的修正值。
5.3 本地录像文件的处理建议
如果你需要把PS流存成录像文件,建议直接存成MP4而不是PS文件。PS文件没有索引,拖动播放非常痛苦。正确做法是解析PS流提取裸流,再用MP4 muxer封装,关键帧写入索引。
录制时还要注意关键帧间隔。海康设备默认I帧间隔是50帧(2秒),如果想做秒级录像回放,需要在SDP协商时向设备请求更小的I帧间隔。国标协议里通过SDP的sprop参数里的sar字段控制,但这个字段各家实现不一致,海康设备有时会忽略。实际项目中,我会在录像文件里额外记录关键帧位置,配合自定义的时间轴索引,回放体验比纯MP4好很多。
6. 调试过程中容易忽略的细节
最后分享几个调试PS流时容易忽略的细节,这些内容网上的教程很少涉及,都是我实际调试中总结出来的。
6.1 PES长度字段可能不可信
你以为PES包长度字段是准的,实际上部分海康设备在音频PES里填写的长度字段与实际数据长度不一致。如果你依赖长度字段严格跳转,解析到音频PES后就对不齐了。
安全做法是:解析视频PES时,如果长度字段在合理范围内就用;解析音频PES时,最好不依赖长度字段,而是等下一个00 00 01起始码出现。虽然性能差一点,但稳健性提升很大。
6.2 海康国标设备对Invite信令的SDP有特殊要求
国标SDP协商时,海康设备比较挑剔。如果你在SDP里同时请求了TCP和UDP传输,部分固件会优先走TCP,但TCP模式下的PS流解析和UDP模式完全不一样,TCP里没有RTP头,直接是PS流。
另外,SDP里的y=字段(ssrc)如果写错,设备会一直推流,但SSRC不匹配,如果你按SSRC过滤数据就会全丢。建议收到第一个RTP包后,动态学习SSRC,而不是强制校验SDP里协商的SSRC。
6.3 保证多看几个PSM再全局解码
PSM里声明的编码类型理论上在会话生命周期内不变,但海康有些设备在切换分辨率或帧率时,PSM内容会变化。比如从主码流切到子码流后,新的PSM里视频宽高参数更新了,如果你按旧的PSM继续解析,画面会撕裂。
建议每次收到新的PSM都刷新参数缓存,同时在关键帧PES到来时做一次完整性校验。很多小问题,追根溯源都是“配置更新时机不对”。
我自己的习惯是,在生产环境做一个“PSM+关键帧双触发参数刷新”的逻辑,这样无论设备端怎么切换,都能保持正确解码。
写在最后
PS流解析在国标GB/T 28181对接中绕不开。看似只是一堆起始码和长度字段的遍历,实际做好做稳,涉及RTP头动态处理、时间戳恢复、同步机制和异常恢复等细节,每一层都有坑。
如果只记住一句话:拿到RTP流别急着解PS,先确认RTP头长度;解析PS时别依赖单一长度字段,要以起始码为准做状态机跳转;解析出来的裸流,一定要等关键帧再丢给解码器。
这套方法我目前已经在多个海康国标对接项目里验证过,从几路到上百路都没有出过问题。如果你在解析PS流过程中还遇到过其他奇怪的坑,欢迎在评论区分享,大家一起把流程完善得更稳。
本文还有配套的精品资源,点击获取