
简介这份文档面向视频监控平台开发与运维人员聚焦GB/T 28181标准中上下级平台对接的接口实现帮助读者理解平台间数据交换与信息共享的通信机制。内容以平台注册流程为主线完整呈现下级平台主动向上级平台发起REGISTER信令、上级返回401 Unauthorized要求鉴权、下级携带Digest鉴权信息重新注册、上级验证后返回200 OK的完整信令交互并附有真实报文示例涵盖Call-ID、CSeq、Via、WWW-Authenticate、Authorization等关键字段同时延伸讲解平台心跳保活机制与离线判定规则。资源为单个doc文档压缩包约85KB结构紧凑、查阅方便。目前已有291人学习适合需要快速掌握28181对接注册与保活细节、对照报文排查联调问题的开发者参考。1. 28181平台对接接口详解从信令握手到媒体流落地的完整链路如果你手头有一份《28181平台对接接口详解.doc》大概率正卡在某个具体环节上级平台注册不上、目录推送缺通道、实时点播黑屏、录像回放卡在 INVITE 没响应。GB/T 28181 这套标准把 SIP 信令和 RTP 媒体流绑在一起文档里往往只列了字段定义却没告诉你字段之间的时序依赖。真正对接时SIP 注册是入口目录查询是数据基础INVITE 是媒体通道的开关任何一个环节的 From/To 头域、SSRC 编码、SDP 协商对不上后面全盘皆输。这篇笔记按实际对接顺序拆解先讲 SIP 信令栈怎么搭、注册和鉴权怎么过再讲目录和媒体协商的参数怎么填最后把点播、回放、云台控制的接口调用串起来并给出排查清单。适合正在做视频监控平台级联、需要把自家平台接入上级 28181 系统的后端和运维工程师。2. SIP 信令栈搭建与注册鉴权把 401 和 403 挡在门外2.1 为什么 28181 对接必须先吃透 SIP 注册流程GB/T 28181 的对接本质是 SIP 协议栈的互联。上级平台是 SIP 服务器 registrar / proxy 下级平台是 SIP 用户代理 UA 。注册流程走的是标准 SIP REGISTER 方法但 28181 在标准之上加了自己的鉴权扩展和头域要求。很多对接失败不是网络不通而是 REGISTER 消息里的 From、To、Contact、Expires 四个头域没按规范填。From 和 To 必须携带平台编码 20 位十进制数字Contact 里的 IP 和端口必须是下级平台实际可达的 SIP 信令地址Expires 决定注册有效期通常设 3600 秒到期前要主动刷新。鉴权走 WWW-Authenticate 挑战上级返回 401 时携带 nonce 和 realm下级用 MD5 算法算出 response 再发一次 REGISTER。如果第二次还返回 403说明用户名密码或 realm 计算有误。提示平台编码不是随便编的前 8 位是行政区划代码中间 6 位是行业编码后 6 位是设备序号。编码不对上级平台可能直接丢弃注册请求。2.2 用 Python 搭一个最小 SIP 注册客户端下面这段代码用 socket 直接构造 SIP REGISTER 消息不依赖重型 SIP 栈适合快速验证注册链路是否通。实际生产环境建议用 PJSIP 或 Sofia-SIP但调试阶段手搓消息能让你看清每个字段。import socket import hashlib import re SIP_SERVER 192.168.1.100 # 上级平台 SIP 服务器 IP SIP_PORT 5060 # 标准 SIP 端口 PLATFORM_ID 34020000002000000001 # 下级平台编码20 位 PASSWORD 12345678 # 对接密码 LOCAL_IP 192.168.1.200 # 下级平台信令 IP LOCAL_PORT 5060 def build_register(cseq, auth_headerNone): 构造 REGISTER 消息auth_header 为 None 时发首次挑战 call_id f{PLATFORM_ID}{LOCAL_IP} from_tag abc123 msg ( fREGISTER sip:{SIP_SERVER} SIP/2.0\r\n fVia: SIP/2.0/UDP {LOCAL_IP}:{LOCAL_PORT};branchz9hG4bK{ cseq }\r\n fFrom: sip:{PLATFORM_ID}{SIP_SERVER};tag{from_tag}\r\n fTo: sip:{PLATFORM_ID}{SIP_SERVER}\r\n fCall-ID: {call_id}\r\n fCSeq: {cseq} REGISTER\r\n fContact: sip:{PLATFORM_ID}{LOCAL_IP}:{LOCAL_PORT}\r\n fMax-Forwards: 70\r\n fExpires: 3600\r\n fUser-Agent: MyGB28181Client/1.0\r\n ) if auth_header: msg auth_header \r\n msg Content-Length: 0\r\n\r\n return msg def parse_challenge(response): 从 401 响应中提取 realm 和 nonce realm re.search(rrealm([^]), response) nonce re.search(rnonce([^]), response) return (realm.group(1) if realm else None, nonce.group(1) if nonce else None) def calc_response(username, realm, password, nonce, methodREGISTER, uriNone): MD5 鉴权计算 ha1 hashlib.md5(f{username}:{realm}:{password}.encode()).hexdigest() ha2 hashlib.md5(f{method}:{uri}.encode()).hexdigest() return hashlib.md5(f{ha1}:{nonce}:{ha2}.encode()).hexdigest() sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((LOCAL_IP, LOCAL_PORT)) sock.settimeout(5) # 第一次注册不带鉴权 sock.sendto(build_register(1).encode(), (SIP_SERVER, SIP_PORT)) try: data, _ sock.recvfrom(4096) resp data.decode(errorsignore) if 401 in resp: realm, nonce parse_challenge(resp) uri fsip:{SIP_SERVER} response calc_response(PLATFORM_ID, realm, PASSWORD, nonce, REGISTER, uri) auth (fAuthorization: Digest username{PLATFORM_ID}, frealm{realm}, nonce{nonce}, furi{uri}, response{response}, algorithmMD5) sock.sendto(build_register(2, auth).encode(), (SIP_SERVER, SIP_PORT)) data2, _ sock.recvfrom(4096) print(第二次注册响应:, data2.decode(errorsignore)[:200]) except socket.timeout: print(超时检查上级平台 IP 和端口是否可达)这段代码的逻辑分三步先发一个不带 Authorization 头的 REGISTER上级返回 401 并携带 realm 和 nonce然后用 MD5 算出 response拼出 Authorization 头再发一次最后打印第二次响应。参数上Expires设 3600 秒是常见值有些上级平台强制要求 1800 或 7200以对接文档为准。Call-ID和From里的 tag 在同一个注册会话中必须保持一致否则上级会认为是新会话。如果第二次返回 200 OK说明注册成功返回 403 则检查密码和 realm 是否匹配返回 400 通常是消息格式有误重点看 Via 头的 branch 参数是否以z9hG4bK开头。2.3 注册保活与心跳机制怎么配注册成功后不是一劳永逸。SIP 注册有有效期Expires 到期前必须重新注册否则上级平台会认为下级离线。常见做法是在 Expires 的 80% 时间点发起刷新比如 3600 秒有效期就在第 2880 秒发新 REGISTER。另外 28181 还定义了心跳机制下级平台定期发送 Keepalive 消息 通常是 SIP MESSAGE 方法 上级收到后回复 200 OK。心跳间隔一般设 60 秒超时 3 次即判定离线。心跳消息的 Content-Type 是Application/MANSCDPxmlXML 体里包含CmdType为Keepalive和DeviceID。如果上级平台没收到心跳即使注册还在有效期内也会把设备标记为离线导致目录查询返回空。注意心跳和注册是两套独立机制注册管的是 SIP 信令通道是否可达心跳管的是设备状态是否在线。两者都要配缺一不可。3. 目录查询与媒体协商把通道列表和 SDP 参数对齐3.1 目录查询的 XML 消息体怎么构造注册通了之后下一步是让上级平台知道下级有哪些设备通道。28181 用 SIP MESSAGE 方法承载 XML 消息体CmdType为Catalog的查询请求由上级发起下级回复目录列表。但实际对接中下级也可以主动推送目录变更。目录查询的 XML 结构如下?xml version1.0 encodingGB2312? Query CmdTypeCatalog/CmdType SN1/SN DeviceID34020000002000000001/DeviceID /Query上级发来这个查询后下级要回复一个Response消息CmdType同样是Catalog但包含DeviceList和SumNum。每个Item里要有DeviceID、Name、Manufacturer、Model、Owner、CivilCode、Address、Parental、ParentID、SafetyWay、RegisterWay、Secrecy、Status这些字段。其中Status为ON表示通道在线OFF表示离线。ParentID是父节点编码通常填下级平台编码。RegisterWay为 1 表示符合 28181 标准的设备。def build_catalog_response(device_id, channels): 构造目录查询响应 XML items for ch in channels: items f Item DeviceID{ch[id]}/DeviceID Name{ch[name]}/Name ManufacturerMyCompany/Manufacturer ModelIPC-100/Model OwnerOwner/Owner CivilCode340200/CivilCode AddressAddress/Address Parental0/Parental ParentID{device_id}/ParentID SafetyWay0/SafetyWay RegisterWay1/RegisterWay Secrecy0/Secrecy Status{ch[status]}/Status /Item return f?xml version1.0 encodingGB2312? Response CmdTypeCatalog/CmdType SN1/SN DeviceID{device_id}/DeviceID SumNum{len(channels)}/SumNum DeviceList{items} /DeviceList /Response这段代码把通道列表拼成 XML。关键参数SumNum必须等于DeviceList里Item的数量不一致上级会报错。DeviceID是通道编码通常 20 位前 10 位是设备类型标识后 10 位是序号。Status字段决定上级是否能看到这个通道的实时画面离线通道即使推送了也点不开。编码格式用 GB2312 还是 UTF-8 取决于上级平台要求常见做法是 GB2312但有些新平台已经支持 UTF-8对接前要确认。3.2 INVITE 里的 SDP 协商媒体流能不能通就看这一步目录推送成功后上级平台发起实时点播发送 SIP INVITE 消息SDP 体里包含媒体接收地址和端口。下级收到 INVITE 后要回复 200 OK 并携带自己的 SDP告知媒体发送地址和端口。SDP 里的关键字段mvideo行指定媒体类型和端口cIN IP4指定媒体 IPartpmap指定编码格式和时钟频率。28181 常用 PS 流封装编码格式通常是PS/90000有些平台也支持H264/90000或MPEG4/90000。# 上级发来的 INVITE 中的 SDP 示例 v0 o34020000002000000001 0 0 IN IP4 192.168.1.100 sPlay cIN IP4 192.168.1.100 t0 0 mvideo 6000 RTP/AVP 96 arecvonly artpmap:96 PS/90000 y0100000001y行是 28181 特有的 SSRC 字段10 位十进制数字前 5 位是域标识后 5 位是设备序号。下级回复的 SDP 里mvideo的端口是下级发送媒体流的端口cIN IP4是下级媒体 IP。如果上级发的是recvonly下级回复sendonly如果上级发sendrecv下级也回sendrecv。SSRC 必须上下级一致否则上级收到 RTP 包后无法关联到对应的会话表现为点播黑屏但信令显示 200 OK。提示SDP 里的 IP 地址必须是上级平台实际能访问到的地址。如果下级平台在 NAT 后面SDP 里填内网 IP 会导致上级收不到媒体流需要在 SIP 层做 NAT 穿透或填公网映射地址。3.3 媒体流发送RTP 打包和 SSRC 绑定SDP 协商完成后下级开始向上级指定的 IP 和端口发送 RTP 流。28181 要求用 PS 封装每个 RTP 包负载是 PS 流的片段。RTP 头里的 SSRC 必须和 SDP 里y字段一致。时间戳按 90kHz 递增序列号从随机值开始。实际开发中可以用 FFmpeg 把 H.264 裸流封装成 PS 流再打包 RTP也可以直接用 GB28181 的媒体库。发送时注意 MTURTP 包负载建议不超过 1400 字节避免 IP 分片。如果上级平台收流后花屏或卡顿先检查 SSRC 是否匹配再检查时间戳是否连续。import struct import time def build_rtp_header(seq, timestamp, ssrc, marker0, payload_type96): 构造 12 字节 RTP 头 version 2 padding 0 extension 0 cc 0 first_byte (version 6) | (padding 5) | (extension 4) | cc second_byte (marker 7) | payload_type return struct.pack(!BBHII, first_byte, second_byte, seq, timestamp, ssrc) # 示例发送一个 RTP 包 ssrc 1000000001 # 与 SDP 中 y 字段一致 seq 1 timestamp 0 payload b\x00\x00\x01\xba # PS 流起始码示例 rtp_header build_rtp_header(seq, timestamp, ssrc) # 实际发送时用 socket.sendto(rtp_header payload, (target_ip, target_port))这段代码只构造 RTP 头实际负载需要从 PS 流中按帧切分。payload_type96 是动态负载类型对应 SDP 里的artpmap:96 PS/90000。marker位在每帧最后一个 RTP 包设为 1帮助接收端判断帧边界。timestamp每帧递增 90000/帧率比如 25 帧每秒就递增 3600。SSRC 用 10 位十进制数不要用十六进制有些上级平台会严格校验。4. 实时点播、录像回放与云台控制的接口调用4.1 实时点播的完整信令时序实时点播从上级发 INVITE 开始下级回 100 Trying、200 OK上级回 ACK然后下级开始发 RTP 流。停止点播时上级发 BYE下级回 200 OK停止发流。这个时序里最容易翻车的是 ACK 丢失。如果下级发了 200 OK 但没收到 ACKSIP 栈会重传 200 OK重传多次后上级可能直接发 BYE 终止会话。解决办法是下级在收到 ACK 之前不要开始发流或者发流但保留会话状态等 ACK 到了再正式启动。另一个坑是 INVITE 里的Subject头域有些上级平台用它传递点播类型比如Subject: 34020000001320000001:34020000002000000001,34020000001320000001:0冒号前是通道编码冒号后是 SSRC。下级要解析这个头域来确认点播的是哪个通道。def parse_invite_subject(subject): 解析 INVITE 的 Subject 头提取通道编码和 SSRC # 格式通道编码:平台编码,通道编码:SSRC parts subject.split(,) channel_id parts[0].split(:)[0] ssrc parts[1].split(:)[1] if len(parts) 1 else None return channel_id, ssrc这段解析逻辑处理的是常见格式但不同上级平台的 Subject 格式可能有差异对接时要抓包确认。channel_id用来定位下级哪个通道要推流ssrc用来绑定 RTP 会话。如果 Subject 里没有 SSRC就从 SDP 的y字段取。4.2 录像回放的倍速控制和拖动录像回放和实时点播的信令流程类似区别在 INVITE 的 SDP 里多了u行指定回放类型和时间范围。u的格式是时间范围:回放类型比如u20240101120000:20240101130000:0表示回放 2024 年 1 月 1 日 12 点到 13 点的录像最后的 0 表示按时间回放。倍速控制通过 SIP INFO 方法发送XML 体里CmdType为PlaybackControlSpeed字段设 2 表示 2 倍速设 0.5 表示半速。拖动通过PlaybackControl里的Range字段实现格式是Range: 20240101123000-表示从 12 点 30 分开始播。!-- 倍速控制 INFO 消息体 -- ?xml version1.0 encodingGB2312? Control CmdTypePlaybackControl/CmdType SN2/SN DeviceID34020000001320000001/DeviceID Speed2/Speed /Control回放接口的坑在于时间格式。28181 要求时间用 14 位十进制精确到秒时区是本地时间。如果下级平台用 UTC 时间回放会偏移 8 小时。另外回放流的 SSRC 和实时点播的 SSRC 不能相同否则上级平台会混淆两个会话。常见做法是回放 SSRC 在实时 SSRC 基础上加一个偏移量比如加 100。4.3 云台控制的 PTZ 指令封装云台控制走 SIP MESSAGEXML 体里CmdType为DeviceControlPTZCmd字段是 8 字节十六进制指令。指令格式第一个字节是A5第二个字节是0F第三个字节是组合控制码第四个字节是水平速度第五个字节是垂直速度第六个字节是变倍速度第七个字节是00第八个字节是校验码。组合控制码的位定义bit0 是右bit1 是左bit2 是下bit3 是上bit4 是放大bit5 是缩小bit6 是停止。比如A50F0100000000表示向右移动速度由第四个字节决定。def build_ptz_cmd(direction, h_speed50, v_speed50, zoom_speed0): 构造 PTZ 指令 cmd_map { right: 0x01, left: 0x02, down: 0x04, up: 0x08, zoom_in: 0x10, zoom_out: 0x20, stop: 0x00 } control cmd_map.get(direction, 0x00) cmd fA50F{control:02X}{h_speed:02X}{v_speed:02X}{zoom_speed:02X}00 # 计算校验码前 7 字节求和后取模 256 total sum(int(cmd[i:i2], 16) for i in range(0, 14, 2)) % 256 cmd f{total:02X} return cmd这段代码生成 8 字节 PTZ 指令。h_speed和v_speed范围 0 到 255值越大速度越快。zoom_speed同理。校验码是前 7 字节之和模 256。实际发送时PTZ 指令要放在DeviceControl的 XML 体里通过 SIP MESSAGE 发给上级上级再转发给设备。如果云台不动先检查指令的校验码是否正确再检查上级平台是否支持DeviceControl透传。5. 对接避坑与排查那些让联调卡三天的细节5.1 注册返回 401 后第二次仍然 401现象第一次 REGISTER 返回 401带上 Authorization 头再发还是 401。原因通常是 realm 或 nonce 解析错误或者 MD5 计算的 HA1 用了错误的用户名。28181 里用户名就是平台编码不是自定义的登录名。解决抓包对比 Authorization 头里的 username、realm、nonce、uri、response 五个字段确保和上级返回的挑战一致。uri 通常是sip:上级平台IP不是sip:上级平台IP:端口。5.2 目录推送后上级只显示部分通道现象下级推了 100 个通道上级只显示 20 个。原因可能是 XML 消息体超过 SIP 消息大小限制被截断。SIP over UDP 默认 MTU 1500 字节目录列表大了必须分片发送或者改用 TCP 传输。解决把目录查询响应拆成多个 MESSAGE 发送每个消息带不同的 SN上级收齐后合并。或者把 SIP 传输层从 UDP 改成 TCPTCP 没有 MTU 限制。5.3 点播信令 200 OK 但画面黑屏现象INVITE 收到 200 OKACK 也发了但上级平台画面黑屏。原因通常是 SSRC 不匹配或媒体 IP 不可达。解决先确认下级发送的 RTP 包 SSRC 和 SDP 里y字段一致再确认 SDP 里的cIN IP4地址上级能 ping 通。如果下级在 NAT 后面需要在 SDP 里填公网映射地址或者在 SIP 层做 NAT 穿透。5.4 回放流时间戳跳变导致花屏现象录像回放时画面花屏或卡顿。原因通常是回放流的时间戳没有按 90kHz 连续递增或者拖动后时间戳重置。解决回放流的时间戳要基于录像文件的绝对时间计算拖动后从新位置的时间戳继续递增不要归零。另外 PS 流的封装要保证每帧有完整的起始码和结束码否则解码器无法识别帧边界。5.5 云台控制指令发了没反应现象PTZ 指令通过 SIP MESSAGE 发出上级返回 200 OK但云台不动。原因可能是指令格式错误或上级平台没有透传。解决先检查 PTZ 指令的 8 字节格式特别是校验码。再确认上级平台的DeviceControl配置是否允许透传 PTZ 指令。有些上级平台只支持预置位调用不支持连续移动指令需要改用PresetCmd。6. 用抓包和日志把对接问题定位到字段级对接 28181 最有效的手段是抓包。在 Linux 上用 tcpdump 抓 SIP 和 RTP 包用 Wireshark 分析。SIP 包看 REGISTER、INVITE、ACK、BYE 的时序和头域RTP 包看 SSRC、序列号、时间戳。如果 SIP 走 UDP抓包命令是tcpdump -i eth0 -w sip.pcap port 5060RTP 端口范围通常是 6000 到 8000抓包命令是tcpdump -i eth0 -w rtp.pcap portrange 6000-8000。Wireshark 里用sip过滤器看信令用rtp过滤器看媒体流。如果 RTP 包 SSRC 和 SDP 不一致Wireshark 会标红。日志方面下级平台的 SIP 栈要打开调试日志记录每条发送和接收的消息。重点看三个地方REGISTER 的 Authorization 头、INVITE 的 SDP 体、MESSAGE 的 XML 体。如果上级平台有日志对比两边的 SN 和 DeviceID 是否一致。常见问题是下级发的 SN 从 1 开始上级期望的 SN 从 100 开始导致消息被丢弃。提示抓包时注意 SIP 消息里的Content-Length必须和实际 XML 体长度一致不一致会导致解析失败。有些 SIP 栈自动计算有些需要手动填。一个具体的技巧把每次对接的 SIP 消息按时间顺序导出成文本用 diff 对比正常和异常场景的消息差异。比如注册成功和注册失败的消息差异往往就在 Authorization 头的 response 字段。这个方法比逐行看日志快得多。我自己的习惯是每次联调前先抓一份正常的注册和点播包作为基线出问题时直接 diff通常十分钟内能定位到字段级。希望帮到你。本文还有配套的精品资源点击获取