开发心得:信令字段与SDP来源——平台、设备、媒体服务易混淆点
在实际对接 GB/T 28181 拉流时,INVITE 最常踩坑的不是「会不会发 SIP」,而是 Via / From / To / Call-ID / Contact / Subject等 以及 SDP 里每个数到底代表信令平台、IPC/NVR 还是流媒体(MS)。填错一种身份,现象往往是:设备 408/481、200 但无流、ACK 对不上、或 MS 根本收不到 RTP。
本文围绕 本仓库现行实现(GBSSender.VideoLiveInvite、AckReq、ParseToRequest、types.Request)说明各字段从哪里来,并总结排障时容易搞混的点。可与 详解-国标与工程实践 对照阅读。
1. 身份
| 身份 | 典型配置 / 数据 | 在 INVITE 里常出现在哪 |
|---|---|---|
| 信令平台 | Config.Sip.ID、rule.Setting 推导的 SipIP()、Config.Sip.Port、Config.Sip.Domain | From、Contact、Via 的主机/端口、Subject 后半「平台 ID」 |
| 设备 | 注册时写入的 types.Request:Source(对端地址串)、TransportProtocol、Original、DeviceAddr | Request-URI / To 的 host 选型、Via 的 transport |
| 媒体服务器(MS) | SipVideoLiveInviteMessage 里 MediaServerIP / StreamPort(先 MS RTPPub 再 INVITE) | SDP 的 o= / c= / m=,与信令监听 IP |
核心:信令From=本平台、To=通道(或设备域内 URI);流媒体谁收RTP一般是 SDP 指向 MS,不要把 Sip 监听地址抄进 c= 除非 MS 与信令同一台机器且你明确要这么做。
2. SIP 头
实现集中在 core/app/sev/vss/internal/pkg/sip/gbs_send.go 的 doMakeHeader、toAddress、VideoLiveInvite。
2.1 Via
case headerTypeVia:
var port = uint16(l.svcCtx.Config.Sip.Port)
return sip.ViaHeader{
{
ProtocolName: "SIP",
ProtocolVersion: "2.0",
Transport: l.req.TransportProtocol,
Host: l.setting.SipIP(),
Port: (*sip.Port)(&port),
- Host / Port:平台侧对外信令地址(协议栈回复路径依赖它),来自
SipIP()+Sip.Port。 - Transport:来自
l.req.TransportProtocol,而l.req是设备注册解析出的types.Request(ParseToRequest+getTransportProtocol)。
易混点:把 Via 写成设备网段 IP,或 transport 与设备实际 REGISTER 不一致(UDP/TCP 混用)。
2.2 From
case headerTypeFrom:
var (
port = uint16(l.svcCtx.Config.Sip.Port)
fromAddr = sip.Address{
DisplayName: l.req.DeviceAddr.DisplayName,
Uri: &sip.SipUri{
FUser: sip.String{Str: l.svcCtx.Config.Sip.ID},
FHost: l.setting.SipIP(),
FPort: (*sip.Port)(&port),
},
Params: sip.NewParams().Add("tag", sip.String{Str: functions.RandWithString("0123456789", 9)}),
}
)
return fromAddr.AsFromHeader()
- User:必须是
Config.Sip.ID(平台 20 位编码),不是设备 ID、不是通道 ID。 - Host/Port:仍是
SipIP()+ 信令端口。 - DisplayName:当前实现复用
l.req.DeviceAddr.DisplayName(来自设备注册 From),与「谁主叫」无关,属展示字段,勿据此推断 URI user。
易混点:From 填成 设备 或 通道,会导致对端认主叫错、后续订阅/录像命名混乱。
2.3 To / Request-URI
toAddress():
func (l *GBSSender) toAddress() *sip.Address {
// 非同一域的目标地址需要使用@host
if l.deviceUniqueId[0:9] != l.svcCtx.Config.Sip.Domain {
return &sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: l.deviceUniqueId},
FHost: l.req.Source,
},
}
}
return &sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: l.deviceUniqueId},
FHost: l.svcCtx.Config.Sip.Domain,
},
}
}
注意:发送直播 INVITE 时,NewGBSSender 的第三个参数传的是 req.ChannelUniqueId(见 SendLogic.VideoLiveInvite),因此这里的 deviceUniqueId 名实不符——实为通道国标 ID。
- User:通道编码(点播目标)。
- Host:跨域时用
l.req.Source(注册来源 / 设备可达信令地址);同域用Sip.Domain。
易混点:
- 把 To 的 user 写成 设备主码 而设备期望 通道 ID。
- Host 误用 平台 SipIP:设备不在该平台域内监听时,应使用 注册时的 Source/domain 规则,否则 INVITE 根本到不了设备。
2.4 Call-ID
- INVITE:新生成随机 Call-ID(
headerTypeCallId)。 - ACK / BYE:必须从 200 OK / 已存对话 里 原样复用(见下一节)。
2.5 Contact
case headerTypeContactCurrent:
var (
port = uint16(l.svcCtx.Config.Sip.Port)
contact = &sip.ContactHeader{
Address: &sip.SipUri{
FUser: sip.String{Str: l.svcCtx.Config.Sip.ID},
FHost: l.setting.SipIP(),
FPort: (*sip.Port)(&port),
},
}
)
- Contact 与 From 一致:平台 ID + 平台信令 IP/端口。
易混点:填设备 Contact 或填 MS 地址——后续 BYE/MESSAGE 可能找不到正确信令终结点。
2.6 Subject
VideoLiveInvite 内拼接 通道 / 播放标志 / ssrc 段 / 平台 ID(实时与回放格式不同)。此头与 SDP 的 SSRC、y= 相关,拼错会导致设备解析流目标错误;细节见 SDP 文第二节。
3. SDP
媒体归属是 MS,不是「信令服务器」
VideoLiveInvite 里 SDP 核心来源:先 MS RTPPub 拿到 StreamPort 等,再写入 SDP(HTTP 逻辑往 SipSendVideoLiveInvite 里塞 MediaServerIP、StreamPort ...)。
var sdpInfo = &sdp.Session{
Version: 0,
Origin: &sdp.Origin{
Username: data.ChannelUniqueId,
Address: data.MediaServerIP,
SessionID: 0,
SessionVersion: 0,
},
Name: functions.Capitalize(string(data.PlayType)),
Connection: &sdp.Connection{Address: data.MediaServerIP},
Media: []*sdp.Media{
{
Type: "video",
Port: int(data.StreamPort),
Proto: proto,
Mode: sdp.ModeRecvOnly,
// ...
SSRC: fmt.Sprintf("%d%s", playFlag, ssrc),
},
},
URI: fmt.Sprintf("%s:0", data.ChannelUniqueId),
}
| SDP 项 | 来源 | 注意点 |
|---|---|---|
o= user | 通道 ID | 设备主码 / 平台 ID |
o= / c= address | MediaServerIP | SipIP()(除非二者同机同网) |
m= port | StreamPort(MS 收流端口) | SIP Sip.Port |
a=recvonly 等 | 平台侧接收 | 与「设备 IP」无关 |
| SSRC 相关 | 通道号切片 + 回放计数 与 Subject 联动 | 可以任意生成 |
易混点:INVITE 头里的 IP 大多是「信令平台」;SDP 里的 IP/端口是「媒体平台(MS)」 两边 NAT、弹性网卡、内外网映射不同时,抄错一方就「信令通、媒体不通」。
4. ACK / BYE
send_sip_proc.go:从 ACK 起 From/To/tag/Call-ID 要与后续一致。
AckReq 实现:
func (l *GBSSender) AckReq(resp sip.Response) (sip.Request, error) {
// ...
to, _ := resp.To()
from, _ := resp.From()
var request = l.makeRequest(
sip.ACK,
[]sip.Header{
l.makeHeader(headerTypeVia),
from,
to,
l.makeHeaderWith(headerTypeCallIdWith, callId),
// ...
},
"",
)
- From / To:直接取自 INVITE 的 200 OK(设备加的 To-tag 已在
To里)。 - 禁止再用「发 INVITE 时的内存 From/To 副本」若与响应不一致。
- Call-ID:用响应里的值,否则对话不匹配。
易混点:自己拼 ACK,把 From/To 又改回「平台→设备」单向视角,忽略 200 OK 对调角色与 tag,设备直接丢弃或回复 481。
5. NewGBSSender
直播 INVITE 调用形态:
inviteData, inviteRes, err := sip2.NewGBSSender(l.svcCtx, req.Req, req.ChannelUniqueId).VideoLiveInvite(req)
req.Req:注册得到的types.Request,提供 设备侧 transport、Source、Original 等。req.ChannelUniqueId:参与toAddress的 user、SDPo=用户名、SSRC 推导等。
若在别的分支误传 设备 ID 当作第三参,会出现 To 错、SSRC/Subject 与通道不一致 一整串问题。
6. 排障清单
- From 的 user 是否是
Config.Sip.ID? - To 的 user 是否是「被点播通道」而不是设备主码?
- To 的 host 是否按「域 + Source」规则指向设备可达信令地址?
- Via 的 transport 是否与该路 REGISTER 一致?
- SDP 的 IP/端口是否 MS 侧收流地址,而不是
Sip.Port? - ACK 是否从 200 OK 抠 From/To/Call-ID,而非重算一套?
7. 小结
- 信令身份:From / Contact / Via 主机端口 → 平台;To user → 通道;To host → 域或注册 Source。
- 媒体身份:SDP o/c/m → MS(
MediaServerIP+StreamPort)。 - 对话延续:Call-ID + 200 OK 的 From/To(tag) 锁定会话,ACK/BYE 必须一致。
把这三类信息记住,比反复抓包猜字段快得多。
文内代码路径:core/app/sev/vss/internal/pkg/sip/gbs_send.go、core/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.go、core/app/sev/vss/internal/pkg/sip/utils.go。