开发心得:信令字段与SDP来源——平台、设备、媒体服务易混淆点

0 阅读5分钟

开发心得:信令字段与SDP来源——平台、设备、媒体服务易混淆点

在实际对接 GB/T 28181 拉流时,INVITE 最常踩坑的不是「会不会发 SIP」,而是 Via / From / To / Call-ID / Contact / Subject等 以及 SDP 里每个数到底代表信令平台IPC/NVR 还是流媒体(MS)。填错一种身份,现象往往是:设备 408/481200 但无流ACK 对不上、或 MS 根本收不到 RTP。

本文围绕 本仓库现行实现GBSSender.VideoLiveInviteAckReqParseToRequesttypes.Request)说明各字段从哪里来,并总结排障时容易搞混的点。可与 详解-国标与工程实践 对照阅读。

项目地址 github.com/openskeye/g…


1. 身份

身份典型配置 / 数据在 INVITE 里常出现在哪
信令平台Config.Sip.IDrule.Setting 推导的 SipIP()Config.Sip.PortConfig.Sip.DomainFromContactVia 的主机/端口Subject 后半「平台 ID」
设备注册时写入的 types.RequestSource(对端地址串)、TransportProtocolOriginalDeviceAddrRequest-URI / To 的 host 选型、Viatransport
媒体服务器(MS)SipVideoLiveInviteMessageMediaServerIP / 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.godoMakeHeadertoAddressVideoLiveInvite

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.RequestParseToRequest + 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

易混点

  1. 把 To 的 user 写成 设备主码 而设备期望 通道 ID
  2. 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),
            },
        }
    )
  • ContactFrom 一致:平台 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 里塞 MediaServerIPStreamPort ...)。

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= addressMediaServerIPSipIP()(除非二者同机同网)
m= portStreamPort(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、SDP o= 用户名、SSRC 推导等。

若在别的分支误传 设备 ID 当作第三参,会出现 To 错、SSRC/Subject 与通道不一致 一整串问题。


6. 排障清单

  1. From 的 user 是否是 Config.Sip.ID
  2. To 的 user 是否是「被点播通道」而不是设备主码?
  3. To 的 host 是否按「域 + Source」规则指向设备可达信令地址?
  4. Via 的 transport 是否与该路 REGISTER 一致?
  5. SDP 的 IP/端口是否 MS 侧收流地址,而不是 Sip.Port
  6. ACK 是否从 200 OK 抠 From/To/Call-ID,而非重算一套?

7. 小结

  • 信令身份From / Contact / Via 主机端口平台To user通道To host域或注册 Source
  • 媒体身份:SDP o/c/mMSMediaServerIP + StreamPort)。
  • 对话延续Call-ID + 200 OK 的 From/To(tag) 锁定会话,ACK/BYE 必须一致。

把这三类信息记住,比反复抓包猜字段快得多。


文内代码路径:core/app/sev/vss/internal/pkg/sip/gbs_send.gocore/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.gocore/app/sev/vss/internal/pkg/sip/utils.go