
简介这是一份围绕GB/T 28181平台对接接口编写的技术参考文档面向安防监控平台开发、集成与维护人员用于解决下级平台与上级平台之间注册、鉴权和心跳保活等核心通信问题。文档以SIP协议为背景详细展示了下级平台主动向上级平台注册的完整信令交换过程先发送REGISTER收到401 Unauthorized后携带用户名、密码等鉴权信息重新REGISTER经上级校验后获得200 OK鉴权采用HTTP Digest机制并基于MD5算法上级平台会通过WWW-Authenticate字段下发realm、nonce等参数。同时文档说明了平台心跳Keepalive机制包括心跳周期需上下级配置一致、连续三次未收到心跳即判定对端离线等判断规则并附有MESSAGE Keepalive示例报文便于直接对照开发与排错。资源仅1个doc文档大小约85KB内容集中、便于快速查阅已有291人学习下载。1. 28181 平台对接接口到底在解决什么问题拿到一份名为 28181 平台对接接口详解.doc 的资料时先别急着往后翻它真正描述的不是一套 HTTP API而是一份上下级平台之间必须共同遵守的“约定”。28181 指的是 GB/T 28181《公共安全视频监控联网系统信息传输、交换、控制技术要求》所谓“平台对接接口”在国标语境下主要是基于 SIP 信令和 RTP 媒体流的对接流程。做过这类对接的团队都有体会设备注册成功了、心跳也正常但上级平台就是看不到通道列表或者目录全部同步好了实况却拉不出来。问题大多出在协议时序和字段理解上而不是代码逻辑本身。这篇笔记按我实际做平台对接时的顺序来写先立住 SIP 信令骨架再讲媒体协商然后给最小可用的代码样例最后把最容易翻车的地方摊开说清楚。2. 用 SIP 信令搭出 28181 对接的最小骨架先说明一个基本认知28181 里的平台对接本质上是把下级平台或前端设备当作一个 SIP 用户代理来接入上级平台。信令走 SIP媒体走 RTP信令和媒体可以分离。也就是说设备 A 通过 SIP 信令在上级平台完成注册和目录上报实际视频流则从设备 A 的媒体端口推送到上级平台指定的媒体服务器端口。很多刚接触这份接口文档的人最大的误解是以为“平台对接接口”像 REST 接口一样发一个 POST 拿到 JSON 就完事。实际上你要处理的是几个信令事务REGISTER、MESSAGE、INVITE、BYE中间还夹着 200、401、ACK 这些响应。GB/T 28181-2016 里注册默认走 UDP 5060但实际项目里也常见 5060 以上的自定义端口。对于平台对接而言我一般建议优先确认三个参数上级平台 SIP 服务器 IP 和端口、下级设备 SIP 服务器 ID20 位国标编码、认证密码。如果不确认这三个字段后面所有信令调试都是盲人摸象。2.1 注册与心跳怎么让上级平台先认你这个下级最常见的对接入口就是设备和上级平台之间的 REGISTER 注册。上一级平台作为 SIP 服务器设备作为 UA 客户端。设备发送第一条 REGISTER通常会被服务器回 401 Unauthorized同时带一个 WWW-Authenticate 头里面包含 realm、nonce。设备拿到这个挑战值后再带 Authorization 摘要鉴权重发 REGISTER。下面是一条不带鉴权的初始 REGISTER 报文用来观察结构REGISTER sip:34020000002000000001192.168.1.100:5060 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.10:5060;branchz9hG4bK123456 From: sip:34020000001320000001192.168.1.10;tagabc123 To: sip:34020000002000000001192.168.1.100 Call-ID: reg-20240511-001192.168.1.10 CSeq: 1 REGISTER Contact: sip:34020000001320000001192.168.1.10:5060 Max-Forwards: 70 Expires: 3600 Content-Length: 0这里面最容易配错的是 From 和 To 的地址。From 是设备自己的国标编号To 是上级平台 SIP 服务器的国标编号两者不能写反。Contact 头要写设备真正能收到信令的 IP 和端口。branch 参数是事务标识不能重复重发 REGISTER 时必须生成新的 branch。Call-ID 在整个注册会话周期内可以保持稳定也可以每次变化但为了保证上级平台追踪状态通常我会用一个固定格式加时间戳生成。心跳用的是 MESSAGE 方法请求体是一个 XML 格式的 Keepalive 消息。它的作用是告诉上级平台“我还在线”。很多设备会在注册后立即开始发心跳如果心跳间隔太短会加重服务器压力太长又容易被判定离线。常用设置是 30 到 60 秒一次Expires 设为 3600 秒两者配合基本能满足绝大多数平台。一个标准的心跳报文大致长这样MESSAGE sip:34020000002000000001192.168.1.100:5060 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.10:5060;branchz9hG4bK789012 From: sip:34020000001320000001192.168.1.10;tagheartbeat123 To: sip:34020000002000000001192.168.1.100 Call-ID: keepalive-20240511-001192.168.1.10 CSeq: 2 MESSAGE Content-Type: Application/MANSCDPxml Max-Forwards: 70 Content-Length: 260 ?xml version1.0? Keepalive CmdTypeKeepalive/CmdType SN1/SN DeviceID34020000001320000001/DeviceID StatusOK/Status /KeepaliveXML 里 DeviceID 必须与 From 头的设备编码一致SN 是序号字段每次心跳递增。Content-Type 必须是Application/MANSCDPxml大小写错误会直接导致上级平台解析失败。这里有一个很容易忽略的坑很多协议栈在发送 MESSAGE 时自动带了Content-Length但如果你手动拼接报文忘记算字节数就会出现“解析失败”。建议在代码里用字符串长度直接计算并填充而不是写死一个值。2.2 目录同步一根查询拉出整棵设备树注册通过之后上级平台要向设备或下级平台查询目录。这个动作同样是 MESSAGEXML 中 CmdType 是 Catalog。上级发一条 Catalog 查询设备回复一条携带通道列表的 XML。查询报文不需要太多字段MESSAGE sip:34020000001320000001192.168.1.10:5060 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.100:5060;branchz9hG4bKQuery01 From: sip:34020000002000000001192.168.1.100;tagplat001 To: sip:34020000001320000001192.168.1.10 Call-ID: catalog-20240511-001192.168.1.100 CSeq: 1 MESSAGE Content-Type: Application/MANSCDPxml Content-Length: 130 ?xml version1.0? Query CmdTypeCatalog/CmdType SN23/SN DeviceID34020000001320000001/DeviceID /Query设备收到后会返回一个 XML里面可能有DeviceList和SumNum。核心节点是Device其中有 DeviceID、Name、Status、Manufacturer、Model、Channel 等。这里需要特别注意的是设备目录和通道目录不是同一层。前端设备本身是一条记录但是摄像头通道可能是嵌套在设备下的子节点。在做平台对接时如果数据库里只有一层设备表后面扩展通道会非常痛苦。我一般会建两张表device 表和 channel 表通过 parent_id 关联这样不管下级平台上报的是“设备通道”还是“纯通道列表”都能无损落库。解析目录 XML 另一个关键点是命名空间。不同厂商的设备返回的响应里经常带上xmlns前缀有的还混有多个命名空间。最稳妥的做法是忽略命名空间只取本地标签名避免因为前缀不同导致解析失败。2.3 云台控制与报警按需扩展的信令子集目录和视频流是必须项云台控制和报警则要看具体项目需求。28181 标准里云台控制走 DeviceControl 指令并携带 PTZCmd 字段。字段内容是一串十六进制字符串协议结构是“头部 控制码 参数 校验”比如F0 14 01 00 00 01 00 00 00 00 00 00 FF这类。实际对接中如果只做视频汇聚平台云台控制可以先放一放如果要做指挥调度或位置联动就必须把控制指令映射成自己的云台协议。报警信息走 Alarm 类型 XML通常上报方式有两种一种是设备主动通过 MESSAGE 上报另一种是上级平台发 Alarm 查询设备返回报警信息。这里要注意国标编码中的“报警编码”和“设备编码”前缀不同报警类型位于编码第 11 到 12 位比如 131 表示视频遮挡、132 表示移动侦测。不要用设备编码来推断报警编码否则会出现报警信息归属错误。3. 媒体流对接从 INVITE 到 RTP 收流的关键参数信令通了之后接下来就是最耗时间的媒体流对接。28181 的视频流不是裸流而是把 PSProgram Stream封装到 RTP 包内传输通常 Payload Type 为 96视频编码可能是 H.264 或 H.265。在平台对接接口里看 Invite 流程是否成功不是看设备有没有回 200 OK而是看 RTP 包有没有到达媒体服务器端口。很多团队在这里被“信令通、视频不通”折磨到怀疑人生。3.1 实况拉流的 INVITE 时序上级平台要拉某通道的实况时发起一条 INVITE请求 URI 是通道的 SIP 地址通常长这样sip:34020000001320000011192.168.1.10:5060。INVITE 的 SDP 里描述了接收媒体的能力。设备收到后如果正常会回 200 OK并在 SDP 里带上它的媒体发送地址和端口。随后上级平台回一条 ACK双方进入媒体传输阶段。一个简化后的 INVITE 请求体如下INVITE sip:34020000001320000011192.168.1.10:5060 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.100:5060;branchz9hG4bKInvite01 From: sip:34020000002000000001192.168.1.100;tag100001 To: sip:34020000001320000011192.168.1.10 Call-ID: invite-20240511-001192.168.1.100 CSeq: 1 INVITE Contact: sip:34020000002000000001192.168.1.100:5060 Content-Type: application/sdp Content-Length: 172 v0 o34020000002000000001 0 0 IN IP4 192.168.1.100 sPlay u34020000001320000011:0 cIN IP4 192.168.1.100 t0 0 mvideo 41000 RTP/AVP 96 arecvonly artpmap:96 PS/90000注意这个 SDP 中mvideo 41000 RTP/AVP 96是上级平台的媒体接收端口arecvonly表示上级只接收。设备的 200 OK 返回的 SDP 里则应该带有设备自己的发送 IP 和端口并标注asendonly。如果你看到设备回的也是recvonly说明角色反了平台侧很可能配成了“被拉流”而没按主动拉流处理。3.2 看懂 SDP 里的地址、端口和 SSRCSDP 看起来很短却有好几个关键字段决定视频能不能通。首先是c和m中的 IP 与端口它们构成 RTP 包的目的地址。其次是y字段这个在标准里是扩展属性表示 RTP 的 SSRC。国标规定 SSRC 遵循设备编码规则通常是一个 10 位数字由设备编码和通道编码推导而来。如果 y 字段缺失或值不对部分上级平台会直接丢弃 RTP 包因为收不到预期的 SSRC。下面的 SDP 是在实际对接中从一台下级平台返回的简化响应v0 o34020000001320000001 0 0 IN IP4 192.168.1.20 sPlay u34020000001320000011:0 cIN IP4 192.168.1.20 t0 0 mvideo 9000 RTP/AVP 96 asendonly artpmap:96 PS/90000 y34020000001320000011注意u字段是“业务类型:发送方编码”实况时是34020000001320000011:0回放时会变成带时间范围的格式。y的值必须和 RTP 头的 SSRC 一致。很多非标准设备会忽略这个字段但对接严格要求时必须补上。媒体端口的选择也有讲究。设备侧回传的mvideo 9000 RTP/AVP 96端口 9000 是设备发送 RTP 的端口不是信令端口。如果这个端口和目标平台媒体服务器之间有防火墙即使信令到 5060 了RTP 包也进不来。所以做平台对接前网络策略必须同时放通信令端口和媒体端口范围。3.3 心跳已通但视频黑屏先查 SDP 协商结果最典型的失败现象是设备在线目录也有但实况黑屏或一直转圈。这时候抓包看 IVITE问题多出在 SDP 协商内容上。常见情况是设备回了 200 OK但 SDP 里的c地址是内网地址上级平台无法访问。比如设备在 NAT 后面信令能出去但 SDP 里的媒体地址仍然是 192.168.x.x第三方平台当然拉不到流。解决思路是在设备侧或下级平台侧启用 NAT 内网穿透配置或者把媒体端口映射成公网 IP并确保 SDP 里c和o都是公网可达地址。另一种黑屏原因是编码不匹配。平台侧要求 PS 封装 H.264设备却发了 H.265或者 Payload Type 不是 96。国标虽然默认支持 H.264但 H.265 在 2016 版里也属于可选编码双方必须显式协商。如果设备 SDP 里rtpmap写的是H265/90000平台不解析就直接丢弃。处理方式是在设备编码能力里设置成平台可接受的编码或者平台侧兼容解析 H.265。4. 用最小代码实现 28181 对接中的三个核心动作文档翻再多不如跑一小段代码让人心里有底。这里我只给出三个最常写的动作注册鉴权、心跳发送、目录解析。它们不是完整可商用的代码而是把接口文档里的关键字段变成可验证的最小实现。实际项目中我会在这个基础上用状态机管理 SACK 时序。4.1 先跑通注册鉴权最小 Python 信令函数SIP 鉴权走的是 Digest 认证算法通常是 MD5。下面这段函数模拟设备侧收到 401 后生成 Authorization 头的算法import hashlib def sip_digest(username, realm, password, nonce, uri, method): # 计算 HA1 MD5(username:realm:password) ha1_input f{username}:{realm}:{password} ha1 hashlib.md5(ha1_input.encode(utf-8)).hexdigest() # 计算 HA2 MD5(method:uri) ha2_input f{method}:{uri} ha2 hashlib.md5(ha2_input.encode(utf-8)).hexdigest() # response MD5(HA1:nonce:HA2) resp_input f{ha1}:{nonce}:{ha2} response hashlib.md5(resp_input.encode(utf-8)).hexdigest() auth_header ( fDigest username{username}, frealm{realm}, fnonce{nonce}, furi{uri}, fresponse{response} ) return auth_header # 实际调用时从服务器 401 响应的 WWW-Authenticate 头中提取 realm 和 nonce auth sip_digest( username34020000001320000001, realm34020000002000000001, password12345678, nonceabcd1234, urisip:34020000002000000001192.168.1.100:5060, methodREGISTER ) print(auth)这个函数里的四个入参对应着报文中的关键鉴权字段username 是设备国标编码realm 和 nonce 不是自己编出来的必须从服务器 401 响应里动态读取。很多对接翻车就是因为把 nonce 写死导致每次重发 REGISTER 都被判定为鉴权失败。另一个容易忽略的是method参数必须保持大写 REGISTER不能小写。如果换成了 MESSAGE 心跳这里的 method 也要跟着变因为 HA2 算法强依赖方法名。实际代码中还需要处理 qop国标平台很多支持 qopauth此时 response 要加cnonce和nc参数逻辑会更复杂一点。有了鉴权头你才能把 REGISTER 拼完整。这个函数的返回值可以直接拼入第二条 REGISTER 报文的 Authorization 头不用再手动算一遍散列值。4.2 目录同步 XML 解析用本地名称绕开命名空间收到设备的目录响应后要做的是从 XML 中提取设备和通道信息。很多设备返回的 XML 会带命名空间直接按带前缀的标签取会踩坑。更稳妥的做法是用 ElementTree 的tag.split(})[-1]忽略命名空间。import xml.etree.ElementTree as ET def parse_catalog(xml_text): root ET.fromstring(xml_text) devices [] # 忽略命名空间提取所有 Device 节点 for dev_node in root.iter(): tag dev_node.tag.split(})[-1] if tag ! Device: continue item {} for child in dev_node: child_tag child.tag.split(})[-1] if child.text: item[child_tag] child.text.strip() devices.append(item) return devices xml_sample Response CmdTypeCatalog/CmdType DeviceList Count1 Device DeviceID34020000001320000001/DeviceID Name前端设备01/Name StatusON/Status ManufacturerExample/Manufacturer /Device /DeviceList /Response for d in parse_catalog(xml_sample): print(d[DeviceID], d[Name], d[Status])这段代码的重点不是把 XML 变成字典而是规避命名空间问题。实战中设备响应里还会出现ChannelList节点和Channel子节点里面又是另一个 DeviceID。你需要在解析时区分配置Device节点下的子节点和Channel节点下的子节点字段名有重叠但含义不同。例如 Device 下的 DeviceID 是设备编码Channel 下的 DeviceID 是通道编码二者不能混用。我的做法是先判断当前节点的父节点顺序再决定该行写入 device 表还是 channel 表。4.3 把心跳和目录查询封装成“对接接口”在实际平台对接接口中我不会每次都手工拼完整报文而是把发送 MESSAGE 的动作封装成一个函数传入 CmdType 和 XML 体。这样后续调试时只需要看日志里某个 CmdType 的请求和响应就能快速判断问题发在哪个环节。import socket def send_sip_message(sock, sip_server, device_id, platform_id, cmd_type, xml_body, cseq): from_uri fsip:{device_id}{sip_server[0]} to_uri fsip:{platform_id}{sip_server[0]} request_line fMESSAGE sip:{platform_id}{sip_server[0]}:{sip_server[1]} SIP/2.0 via fVia: SIP/2.0/UDP 192.168.1.10:5060;branchz9hG4bK{cseq} headers [ request_line, via, fFrom: {from_uri};tagtag{cseq}, fTo: {to_uri}, fCall-ID: msg-{cseq}192.168.1.10, fCSeq: {cseq} MESSAGE, Content-Type: Application/MANSCDPxml, fContent-Length: {len(xml_body)}, ] message \r\n.join(headers) \r\n xml_body sock.sendto(message.encode(utf-8), sip_server) # 构造 Keepalive 心跳的 XML 体 heartbeat_xml ?xml version1.0? Keepalive CmdTypeKeepalive/CmdType SN1/SN DeviceID34020000001320000001/DeviceID StatusOK/Status /Keepalive # 使用示例 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) send_sip_message(sock, (192.168.1.100, 5060), 34020000001320000001, 34020000002000000001, Keepalive, heartbeat_xml, cseq2)这段代码把 SIP 地址拼装和发送分离XML 体必须提前按标准格式生成。Content-Length必须严格等于xml_body的字节长度少一个字符都会让对端卡在解析阶段。注意xml_body有 BOM 或中文编码问题时长度需要用 UTF-8 字节数计算而不是用 Python 的len()算字符数因为len数的是字符不是字节。cseq在循环发送里必须递增同一个 cseq 重发会导致对端只接收第一次消息。这段函数没有处理鉴权真实使用时还需要加上 Authorization 头但其余拼接逻辑可以直接复用。5. 平台对接接口的常见坑与排查手段5.1 注册上去五秒就被踢鉴权算法与时间戳不同步现象设备注册后显示在线几秒后上级平台自动把它踢下线。原因最常见的是设备发送 REGISTER 的时间戳或 nonce 与平台期望不一致。平台返回 401 后携带有过期时间的 nonce有的实现要求客户端在收到 401 后立即使用该 nonce 重发如果中间夹了其他心跳消息导致延时nonce 过期就会踢下线。另一些厂家在调试时把服务器时间改到过去导致 Expires 现场判定失败。解决在代码中编写基于状态机的注册流程收到 401 后立即计算 Authorization 并重发 REGISTER中间不插入任何其他消息同时校对设备与平台的 NTP 时间误差超过 30 秒就要先校时。5.2 目录同步只有一个根节点XML 命名空间和编码问题现象上级平台查询目录设备响应了 200 OK但解析后只有一条 Device且通道列表为空。原因常见于设备返回的 XML 带有命名空间前缀平台解析器按固定标签名匹配正好命中Device但通道节点可能叫ChannelList与标准不完全一致。另一个原因是 XML 编码不是 UTF-8而是 GBK中文字段乱码导致解析中断。解决解析框架必须忽略命名空间本地标签名比较对响应报文先按 UTF-8 解码失败再尝试 GBK。同时打印原始报文看看通道列表是空节点还是孩子节点嵌套。5.3 平台显示在线但不出视频SSRC 没按规则编码现象信令正常INVITE 和 200 OK 都完成RTP 包却不被接收。原因部分上级平台对 RTP SSRC 有白名单校验要求 SSP RC 必须等于 SDP 中y字段的值。如果设备没实现 y 字段或实现成随机值平台会丢弃 RTP 包。解决抓包对比 SDP 的y与 RTP 包头 SSRC。如果两者不一致优先改设备配置关闭随机 SSRC平台侧若允许配置则关掉严格校验。真正在项目里快速定位这个问题的办法是看媒体服务器有没有统计到 RTP 包有包但点不了画面第一怀疑就是 SSRC。5.4 信令成功但流只有一片黑SPS/PPS 转封装丢失现象RTP 包有接收但解码器不出画面只有黑屏或花屏。原因28181 的 PS 流里 H.264 的 SPS/PPS 信息位于关键帧起始处有些转码设备把它们放在了最前面而上级平台的拆流模块没有等齐 SPS/PPS 就开始推给解码库。解决平台侧把 RTP 封包需要解析 PS 头对于 H.264 要等 IDR 帧连同 SPS/PPS 一起再送到解码器。如果设备支持配置 PS 封装关闭“SPS/PPS 独立于 IDR 发送”改为打包到同一个 PES 里送回。这个现象很隐蔽信令没有任何报错只能通过查看收到的 PS 包十六进制确认里面有没有完整 SPS 块。5.5 抓包三板斧看 REGISTER、INVITE、Keepalive排查 28181 对接问题不要整段抓包看而是按三类报文分头看。先看 REGISTER 是否带鉴权是否有 200 OK再看 Keepalive 是否周期性到达最后看 INVITE 协商后的媒体源地址端口。抓包过滤可以这样写sip只管信令udp.port9000只管媒体。如果信令完全正常却收不到 RTP就把过滤器改成目标媒体端口。这样三步走下来80% 的问题都能定位到具体模块。处理完一个现象我会把抓包文件保存下来标注当时的平台版本和固件时间避免换版本后同样的问题“复活”。6. 把接口文档变成可验收用例上线前必做的五步拨测接入一个新平台前我会把文档里的接口清单翻译成五条可验收的拨测用例每一条都能在十分钟内判断协议栈是否合格。第一步冷启动注册清空所有会话状态重启设备确认第一条 REGISTER 收到 401 后能完成二次带鉴权注册。第二步观察心跳稳定性连续运行两小时统计 Keepalive 是否无抖动到达。这里我会把心跳超时阈值设得非常短比如 15 秒宁可频繁告警也不让现场“假在线”隐藏问题。第三步目录轮询主动发三次 Catalog 查询比对通道数量是否一致。如果第三次和第一次不一样说明设备侧目录生成有随机丢失这比一次解析失败更危险。第四步实况拉流并发同一通道同时拉两路实况确认 SDP 里的媒体端口不会打架。很多低端设备只有一个媒体端口不支持并发需要在平台侧限制单路预览。第五步断网重连把设备网线拨掉再插上观察设备是否能在 60 秒内重新注册并恢复心跳。最后我习惯保留一份拨测记录表日期、平台软件版本、设备固件版本、信令抓包文件、媒体端口范围、SSRC 编码方式、通过/失败原因。这个习惯救过我很多次尤其当设备厂家换固件后对接参数悄悄变化翻一下记录就知道该改哪几个字段。28181 这类对接本质上没有玄学每一步都有信令报文和媒体包可以做证据只要愿意沉下去抓包绝大多数问题都能在一个工作日内解决。希望这份从信令骨架到拨测方法的实战笔记能帮你在下一次 28181 平台对接里少踩几个坑。本文还有配套的精品资源点击获取