首页 / 资讯中心 / 文章详情

海康国标PS流解析:从RTP到裸H.264的完整实战指南

海康国标PS流解析:从RTP到裸H.264的完整实战指南 ★ FEATURED ARTICLE
简介海康、国标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 IDPES头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:pos3] 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[pos1]直接左移22位但标准规定第一字节只有低3位参与拼接中间字节和数据字节也要先去掉最低位的marker bit。不剔除marker bitPTS值会有肉眼不容易发现的偏差播放器表现就是按帧跳或者音画不同步。时间戳单位是90kHz也就是PTS值除以90才是毫秒。国标平台对接时很多SIP服务器会要求RTP时间戳和PS里的PTS对齐建议解析器把两个时间戳同时打印出来比对。我一般会在调试日志里输出一行RTP_TSxxxxx PTSxx.xxx PTS_delta_msxx一眼就能看出差多少。3. 海康设备实战从RTP收流到裸H.264输出的完整链路海康设备的流媒体行为有几个固定规律RTSP取流时RTP Payload Type通常为96PS流封装格式遵循MPEG-PS但偶尔会带私有扩展关键帧前必带SPS/PPS。这些规律在接入前最好先通过抓包确认别拿来就用。3.1 海康RTP传PS先看SDP再动手解析海康流之前先拉一下SDP里面写明了封装方式和编码参数。ffprobe -v error -show_entries streamindex,codec_name,codec_type -show_entries formatformat_name \ -rtsp_transport tcp -i rtsp://192.168.1.64:554/Streaming/Channels/101SDP里能看到类似artpmap:96 PS/90000和afmtp: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和PPSType 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包缓存到marker1再处理。4. 国标平台接入把PS流处理成平台愿意认的样子国标GB28181的媒体传输基本可以理解为「SIP信令 RTP/PS流」。设备向SIP服务器发起INVITE服务器返回200 OK后媒体流通过RTP发送。这里最容易出问题的不是传输而是PS流打包细节。4.1 国标SDP里对PS流的约定Payload类型、封装方式按GB/T 28181-2016的要求视频RTP负载格式为PSRTP Payload Type为96。下面是一段设备注册后常见的SDP响应v0 o1000000000 0 0 IN IP4 192.168.1.64 sPlay cIN IP4 192.168.1.64 t0 0 mvideo 6000 RTP/AVP 96 artpmap:96 PS/90000 asendonly参数说明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(scrpts * 300) # SCR是27MHz时钟 psm build_psm([0xE0, 0xC0]) pes build_pes(0xE0, video_payload, pts, dts) return pack_header psm pes逻辑说明SCR的单位是27MHzPTS是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里如果带了错误的mvideo 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时间戳是90kHzPTS也是90kHz换算成毫秒后两者差值应该在100ms以内。如果差得远检查PTS解析是否漏了marker bit。另一个常见错误是把PES头里的数据长度当成PTS位置导致读取了错误字节。建议解析器加一个断言assert (b[0] 4) 0x2 or (b[0] 4) 0x3在PTS解析前验证标志位。5.3 现象海康回放流解析总是失败原因回放流和实时流的结构不一样。实时流的每个PES基本对应一帧而回放流会在一段PS里连续存放多个帧的PESI帧前面还会带私有扩展解析器按实时流的「一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 framepts_time,pict_type,key_frame \ -f mpeg -i captured_stream.ps | head -50这里的-f mpeg指定输入格式为MPEG-PS。如果输出里pts_time有规律递增、key_frame1的帧数接近预期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验证 - 解析器统计对比」的闭环不再靠肉眼看过不过。这个习惯帮我挡掉了好几次改动引起的回归问题。做流媒体解析工具的对照验证比多写几十行解析代码更有用希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站