☰
海康国标PS流解析:从RTP到裸H.264的完整实战指南
2026/10/16 2:08:12 网站建设 项目流程

简介:海康、国标PS流解析的资源包,聚焦音视频流媒体中的MPEG-2 PS流处理,面向需要对接海康设备或国标GB/T 28181场景的开发者。包内提供两套实现方案:基于ffmpeg的解析工程与不依赖第三方库的直接解析版本,可帮助读者对比学习PS流解封装原理、PTS/DTS时间戳提取以及音视频数据包分离等关键环节。资源共27个文件,以cpp源码和h头文件为主,附带sln/vcxproj工程配置、tlog/obj等编译中间产物及调试日志,压缩包大小2.24MB,结构上按“ffmpeg解析”与“直接解析”两个模块组织,便于按需查看。已有1096人学习下载,适合具备一定音视频基础、希望深入掌握PS流内部机制的开发者。

1. 海康国标设备的PS流:为什么说拿到RTP只是第一步

做音视频流媒体接入的人,第一课往往不是H.264,而是PS流。海康设备走国标平台、或者用私有SDK取流时,RTP包里很少直接装裸码,绝大多数情况是先把H.264和AAC复用成PS流,然后再切片塞进RTP。如果你一上来就按字节找00 00 00 01 67去抓SPS/PPS,十有八九会对不上,因为PS层本身有一套起始码和长度字段,把真正属于编码层的数据包在里面。这篇文章要解决的就是这个:PS流到底是什么结构、海康设备一般怎么吐PS流、国标平台对PS流有什么具体约定,以及解析过程中最容易踩的五个坑。做平台接入、拉流解析、转封装的同学可以照着抄思路。

2. PS流协议拆解:从0x000001BA到PES,解析器的骨架就这一套

PS流全称Program Stream,是MPEG-2系统层的一种复用方式,和TS流是同一套体系下的不同分支。TS按188字节固定包长切分,适合有损传输;PS按逻辑包复用,适合本地文件、光盘和低丢包实时传输。国标GB28181和大部分安防设备选择PS,核心原因是PS包长度可变,可以把一帧的编码数据尽量塞在一个PS包里,接收端按包解析,不用像TS一样做PID重组。

2.1 PS流的三层骨架:Pack头、PSM、PES

拿到一段PS流,解析顺序可以固定为:定位Pack头、跳过系统头、找PSM、进入PES层。每一层都有固定的起始码,建议直接把下面三组码值写死在解析器常量里。

层级起始码(十六进制)说明
Pack头00 00 01 BAPS流包起点,包含SCR、节目复用速率
系统头00 00 01 BB可选,描述码流中的流类型和缓冲参数
PSM00 00 01 BC节目流映射,列出视频流、音频流的Stream ID
PES头00 00 01 Ex/CxE0-EF是视频,C0-DF是音频,Bx是私有流

我一般会先把整个缓冲区的数据做一次扫描,把所有起始码的位置和类型记录下来,形成一个偏移表。这样后续解析PES、提取帧数据就不需要反复回溯缓存。注意PSM不一定在每一个PS包中都出现,很多海康设备只在关键帧前的PS包里附带PSM,常规帧只给PES。所以解析器不能依赖PSM做流识别,要直接认Stream ID。

def scan_start_codes(data): results = [] idx = 0 while idx < len(data) - 3: if data[idx] == 0x00 and data[idx + 1] == 0x00 and data[idx + 2] == 0x01: code = data[idx + 3] if code in (0xBA, 0xBB, 0xBC) or 0xC0 <= code <= 0xEF: results.append((idx, hex(code))) idx += 4 else: idx += 1 else: idx += 1 return results

这段脚本的作用是把PS流里的包边界全部找出来,返回每个起始码的偏移和类型。解析器拿这个结果做二次遍历,就不用每次从头找起始码。需要注意,NALU的起始码00 00 00 01也会被这个逻辑扫到,但NALU的第三个字节是NALU Type,通常是67、68、65、61等,不在BA到EF的区间范围内,所以不会和PS层冲突。

2.2 一个最小可用的PS解析器:先定位PES,再提裸流

扫完起始码后,最关键的动作是解析PES头。PES头由三部分构成:4字节起始码前缀、2字节PES长度、若干字节PES可选头。视频PES的包长度字段经常是0,表示长度不固定,需要一直读到下一个PES头或者流结束。

def parse_pes_header(data, pos): # data是完整PS缓冲,pos指向某个起始码位置 assert data[pos:pos+3] == b'\x00\x00\x01' stream_id = data[pos + 3] packet_len = int.from_bytes(data[pos + 4:pos + 6], 'big') header_data_len = data[pos + 8] & 0xFF # 注意:PES头可选字段长度在字节8 pts_dts_flags = (data[pos + 7] >> 6) & 0x03 payload_start = pos + 9 + header_data_len payload_end = None if stream_id != 0xE0: # 视频PES长度经常为0 if packet_len > 0: payload_end = pos + 6 + packet_len return { 'stream_id': stream_id, 'pts_dts_flags': pts_dts_flags, 'payload_start': payload_start, 'payload_end': payload_end, }

逻辑说明:PES可选头从第9个字节开始,第一个字节是可选头长度,所以真实负载起点是pos + 9 + header_data_len。PTS/DTS标记在字节7的高两位,只有这里为2或3时,PES头里才包含时间戳。音频PES的长度字段一般有效,视频PES很多设备填0,解析时优先按起始码边界切分。

参数说明:packet_len是PES包长度字段,指的是从下一个字节开始算的有效负载长度,不包含起始码和长度字段本身。遇到0就按「遇到下一个起始码结束」处理,这是最稳妥的做法。

2.3 时间戳不是随便取的:PTS/DTS该怎么读

PTS/DTS在PES头里占了5个字节,但不是直接拼接成整数,而是把33位基准值拆成3段塞进去。解析方法每个做流媒体的人都要背一下:先取出5字节,第一字节的高3位是标志位,需要剔除;剩下4字节加最后一字节,拼成33位值。

def parse_pts(data, pos): b0 = data[pos] pts = ((b0 >> 1) & 0x07) << 30 pts |= data[pos + 1] << 22 pts |= ((data[pos + 2] >> 1) & 0x7F) << 15 pts |= data[pos + 3] << 7 pts |= (data[pos + 4] >> 1) & 0x7F return pts

这段代码的事故高发点在移位。很多人都把data[pos+1]直接左移22位,但标准规定第一字节只有低3位参与拼接,中间字节和数据字节也要先去掉最低位的marker bit。不剔除marker bit,PTS值会有肉眼不容易发现的偏差,播放器表现就是按帧跳或者音画不同步。

时间戳单位是90kHz,也就是PTS值除以90才是毫秒。国标平台对接时,很多SIP服务器会要求RTP时间戳和PS里的PTS对齐,建议解析器把两个时间戳同时打印出来比对。我一般会在调试日志里输出一行:RTP_TS=xxxxx PTS=xx.xxx PTS_delta_ms=xx,一眼就能看出差多少。

3. 海康设备实战:从RTP收流到裸H.264输出的完整链路

海康设备的流媒体行为有几个固定规律:RTSP取流时,RTP Payload Type通常为96;PS流封装格式遵循MPEG-PS,但偶尔会带私有扩展;关键帧前必带SPS/PPS。这些规律在接入前最好先通过抓包确认,别拿来就用。

3.1 海康RTP传PS:先看SDP再动手

解析海康流之前,先拉一下SDP,里面写明了封装方式和编码参数。

ffprobe -v error -show_entries stream=index,codec_name,codec_type -show_entries format=format_name \ -rtsp_transport tcp -i rtsp://192.168.1.64:554/Streaming/Channels/101

SDP里能看到类似a=rtpmap:96 PS/90000和a=fmtp:96 profile-level-id=...。如果rtpmap的编码名是PS,说明RTP负载就是PS流;如果是H264,那说明RTSP自己在做RTP封包,负载里直接是NALU,解析逻辑完全不同。

这里我习惯先抓包而非直接写解析器。用Wireshark抓50个RTP包,看第一个包有没有00 00 01 BA。有就是PS,没有就要检查是否走了私有RTP封装。海康部分老设备会把PS再拆成私有分片,每个RTP包只有PS头的一部分,这时需要根据RTP时间戳和Mark标志位拼包,解析复杂度直接上一个台阶。

3.2 回调流里怎么定位关键帧:SPS/PPS与IDR的配合

PS流解析完成后,下一步是定位I帧。海康设备在I帧前一个PES里,会把SPS、PPS、SEI一并放进去,然后是IDR帧。三个NALU的起始码连在一起出现,00 00 00 01 67、00 00 00 01 68、00 00 00 01 65。

def find_keyframe(ps_data): # 先剥离PS层,再在PES负载里找NALU nalus = [] for start, code in scan_start_codes(ps_data): if code == 0xE0: pes = parse_pes_header(ps_data, start) payload = ps_data[pes['payload_start']:] # 在这里扫描 00 00 00 01 pos = 0 while True: nalu_start = payload.find(b'\x00\x00\x00\x01', pos) if nalu_start == -1: break nalu_type = payload[nalu_start + 4] & 0x1F if nalu_type in (5, 7, 8): nalus.append(nalu_type) # 记录对应的PTS pos = nalu_start + 4 break return nalus

这段代码的处理方式和做裸H.264分析基本一致,重点是拿NALU Type 5是否出现来判断关键帧。一个常见的误区是用PS里第一个PES是不是视频流来判断关键帧,但海康设备在I帧前的PES里会先塞SPS/PPS,所以判断条件应该是「同时出现Type 7、Type 8、Type 5」,三件套齐全才算真关键帧。

参数说明:nalu_type = payload[nalu_start + 4] & 0x1F取的是NALU头部的低5位,对应Type值。Type 7和Type 8对应SPS和PPS,Type 5对应IDR帧,Type 1是非IDR的I slice。如果要精确统计GOP,还需要看P帧数量,这里按需扩展即可。

3.3 输出裸H.264时的时间戳策略

解析出编码数据后,输出给播放器或转推服务时,时间戳优先用RTP时间戳换算,不要直接用PS里的PTS。原因是海康设备RTP时间戳是单调递增的,而PS里的PTS在某些回放流场景会重复或回退,如果直接拿PTS做音视频同步,会出现播放器卡最后一帧的问题。

用RTP时间戳换算毫秒:pts_ms = rtp_ts * 1000 / 90000。RTP时间戳是90kHz,单位换算后就是播放器需要的PTS。如果同时还解析了PS里的PTS,可以在调试阶段把两个值打出来对比,不一致以RTP为准。

class H264Depacketizer: def __init__(self): self.rtp_ts = 0 self.pts_ms = 0 self.pes_cache = b'' def push_rtp(self, rtp_payload, rtp_timestamp, marker): # 统计一个RTP包是否携带有完整PS包 self.rtp_ts = rtp_timestamp self.pts_ms = self.rtp_ts * 1000 // 90000 if marker: # 当前包是这一帧的结束,解析并输出完整H.264 self.flush(rtp_payload)

逻辑说明:大多数海康设备在RTP的marker位为1时,代表当前PS包是一帧的结束。可以在marker包做PES结算,把前面缓存的数据一次性交给解码器。如果marker一直为0,说明这一帧被拆到了多个RTP包,缓存到marker=1再处理。

4. 国标平台接入:把PS流处理成平台愿意认的样子

国标GB28181的媒体传输基本可以理解为「SIP信令 + RTP/PS流」。设备向SIP服务器发起INVITE,服务器返回200 OK后,媒体流通过RTP发送。这里最容易出问题的不是传输,而是PS流打包细节。

4.1 国标SDP里对PS流的约定:Payload类型、封装方式

按GB/T 28181-2016的要求,视频RTP负载格式为PS,RTP Payload Type为96。下面是一段设备注册后常见的SDP响应:

v=0 o=1000000000 0 0 IN IP4 192.168.1.64 s=Play c=IN IP4 192.168.1.64 t=0 0 m=video 6000 RTP/AVP 96 a=rtpmap:96 PS/90000 a=sendonly

参数说明:PS/90000表示负载是PS流,采样时钟90kHz。这里如果写成H264/90000,平台会按照RTP打包H264来解包,直接解析失败。很多SIP服务器日志报“不支持的媒体类型”,第一排查项就是这个。

国标平台对接时,RTP负载不能随便用私有分片。平台侧解析器通常只认标准PS,如果负载里出现非标准扩展字段,解析器会跳过或直接丢弃。所以在海康设备接国标平台之前,一定要确认:设备输出的PS流是否被厂商做了私有扩展。如果做了,需要先剥离再重新封装标准PS。

4.2 从海康PS流到国标PS流:转封装时的规范化

有些海康设备在PS包中会写入厂商私有数据段,存放帧率、GOP长度、丢包统计等自定义信息。国标平台不认识这些字段,遇到不认识的PS私有流可能直接中断解析。处理方式是做一次「PS流清洗」。

清洗流程是:剥离PS私有扩展 -> 重新生成标准PS头 -> 按标准PES打包输出。其中重新生成PS头时,SCR和program_mux_rate可以沿用原值,但PSM必须重新生成,只保留视频Stream ID为0xE0、音频Stream ID为0xC0这两个元素。

def repack_ps(video_payload, pts, dts): pack_header = build_ps_header(scr=pts * 300) # SCR是27MHz时钟 psm = build_psm([0xE0, 0xC0]) pes = build_pes(0xE0, video_payload, pts, dts) return pack_header + psm + pes

逻辑说明:SCR的单位是27MHz,PTS是90kHz,所以换算系数是300。PSM需要先于PES出现,这样国标平台解析器才能在一开始就知道PS流里包含哪些流。如果PES都传完了再给PSM,部分平台解析不到,流出画很慢。

参数说明:build_psm里只声明了E0和C0两个Stream ID,如果设备流里面还有私有流ID B0,这里必须过滤掉,否则平台侧会把私有流当作音视频流解析,触发未知格式告警。

4.3 平台不出画的排查顺序

国标平台接入了但看不到画面,排查顺序很重要。我一般按「信令 -> RTP -> PS -> H264」四层查。信令层看INVITE和200 OK里的SDP是否一致;RTP层看负载类型是不是96,时间戳是否跳变;PS层看是否能扫到BA头;最后才是H264层看SPS/PPS是否齐全。

很多人在第四层花了大量时间查SPS,实际上问题出在第一层。SIP服务器返回的200 OK里如果带了错误的m=video port,设备会把RTP发到错误端口,自然什么都没有。所以接国标平台时的第一件事,不是抓RTP包,而是先看SIP抓包里的媒体端口和实际收发端口是否一致。

5. PS流解析避坑与常见问题排查:五个最容易翻车的地方

PS流解析这个方向,真正难的从来不是起始码,而是各种设备私有行为和时间戳异常。下面五条坑是我在做设备接入和平台联调时反复踩过的,每一条都按现象、原因、解决的顺序写,可以直接对照排查。

5.1 现象:画面花屏且伴随碎帧,偶尔卡死

原因:PS包切分边界没对齐。部分海康设备把一帧拆成多个RTP包时,每个RTP包里只放了PS负载的一部分,没有在每一个RTP包里都放置完整的PS头。解析器如果直接从RTP负载中间开始找BA,会把PES负载错当成PS头来解析,导致花屏。

解决:解析时不要从每个RTP包头部开始,而是先把同一时间戳、连续序号的所有RTP负载拼接成完整缓冲,再从缓冲里扫描00 00 01 BA。拼接的完整缓冲里PS头才是合法的,单独RTP包里的数据可能是上一帧的尾部。

5.2 现象:RTP包序号连续、负载完整,但解码器就是不出画面

原因:PTS/DTS异常。RTP时间戳正常,但PS包里的PTS字段解析错误,播放器拿到了负的PTS增量或倒序PTS,导致帧被丢弃。

解决:打印RTP时间戳和PS PTS的换算值做对比。RTP时间戳是90kHz,PTS也是90kHz,换算成毫秒后两者差值应该在100ms以内。如果差得远,检查PTS解析是否漏了marker bit。另一个常见错误是把PES头里的数据长度当成PTS位置,导致读取了错误字节。建议解析器加一个断言:assert (b[0] >> 4) == 0x2 or (b[0] >> 4) == 0x3,在PTS解析前验证标志位。

5.3 现象:海康回放流解析总是失败

原因:回放流和实时流的结构不一样。实时流的每个PES基本对应一帧,而回放流会在一段PS里连续存放多个帧的PES,I帧前面还会带私有扩展,解析器按实时流的「一PS包一帧」去读,自然错位。

解决:解析回放流时,不依赖PS包边界,而是以PES流为单位循环读取。每解析完一个PES,就继续往后扫下一个00 00 01起始码,直到缓冲取完。同时注意,回放流的关键帧间隔比实时流大很多,不要用默认的GOP大小去做缓冲池扩容。

5.4 现象:国标平台接入后视频延迟持续增大,半小时后卡成幻灯片

原因:缓存队列累积。一些平台服务器在收到PS流后,不会立刻把数据交给解码器,而是先进队列等关键帧。如果PS流里SPS/PPS没有定期出现,平台一直在等关键帧,队列越堆越久。

解决:设备接入国标平台时,设置关键帧间隔不超过2秒,即GOP大小不超过60帧(按25fps算)。同时开启SPS/PPS每个关键帧都携带,避免平台等待第二个关键帧。

5.5 现象:PS流里有音频但解析后播放器没有声音

原因:Stream ID判断漏了音频段。音频PES的Stream ID是0xC0到0xDF,常见格式是0xC0。有的解析器只处理视频E0,把音频PES的数据当成数据垃圾跳过,这没问题。但如果连音频PES的长度也跳错了,就会破坏后续视频PES的边界,导致视频也解码失败。

解决:解析时对所有Stream ID做统一记录,即使不提取音频数据,也要正确跳过它的PES长度。如果AAC音频封装在PS里,PES负载是AAC裸流,需要再按ADTS格式拆帧。国标平台通常要求音频PES的PTS和视频PES的PTS对齐,所以同样要解析音频PTS,否则音画同步只能靠播放器侧做粗对齐。

6. 验证技巧:用Wireshark和ffprobe把PS解析结果变成可核对参数

写解析器最怕什么?怕解析结果只有自己信。同样一段PS流,设备厂商说没问题,平台侧说解析失败,两边都有工具能看,可我这边调试输出只有偏移和长度,没法直接跟别人的工具核对。后来我每次改完解析器,都会强制走一遍下面这套验证流程。

6.1 用ffprobe做结果验证

先把PS流落盘,然后用ffprobe直接探测。ffmpeg的mpeg demuxer能识别PS容器,如果它能解析出正确的时间基和帧数,说明我们手动解析的PTS方向是对的。

ffprobe -v error -show_streams -show_frames -select_streams v:0 \ -show_entries frame=pts_time,pict_type,key_frame \ -f mpeg -i captured_stream.ps | head -50

这里的-f mpeg指定输入格式为MPEG-PS。如果输出里pts_time有规律递增、key_frame=1的帧数接近预期GOP数量,那解析器基本没问题。如果ffprobe也解析失败,就直接说明这个PS流本身就不是标准PS,需要回到抓包阶段看设备吐的到底是什么格式。

6.2 用Wireshark排查RTP与PS对应关系

抓包后先用过滤器定位RTP流:

rtp.ssrc == 0x12345678 && rtp.payload_type == 96

把某个RTP包的负载导出为十六进制文本,手工看前8字节。标准PS负载以00 00 01 BA开头,这是最直接的判断方式。如果看到00 00 01 BE之类不太常见的起始码,说明设备做了私有扩展,需要查明扩展逻辑后再写解析处理。

另外一个好用的技巧是把导出的PS负载直接存成.ps文件,再用ffprobe验证——不需要完整录流,只抓几个包就能判断PS结构是否标准。

6.3 把验证动作固化进解析器

我现在的做法是在解析器里内置一个验证开关:每次解析完一段PS缓冲,自动统计三组数据——PES数量、关键帧数量、PTS增量分布,并和抓包结果对比。如果PES数量对不上RTP包数量,直接打印警告。关键帧数量应为总帧数/GOP大小的上下浮动范围,超出则说明边界识别有问题。PTS增量应稳定在40ms或33ms附近,出现0或者负数就直接中断解析。

从那以后,我每改一次PS解析逻辑,都会强制走一遍「Wireshark抓包 -> ffprobe验证 -> 解析器统计对比」的闭环,不再靠肉眼看过不过。这个习惯帮我挡掉了好几次改动引起的回归问题。做流媒体解析,工具的对照验证比多写几十行解析代码更有用,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询