GB28181 实时流拉取与播放全流程解析:从设备注册到 INVITE 建立媒体通道
简介
在 GB/T 28181 国标视频平台中,实时视频播放是核心功能之一。该过程涉及设备注册、信令交互、媒体服务器(Media Server)协同、SDP 协商等多个环节。尤其关键的是通过 SIP INVITE + SDP 建立 RTP/RTCP 媒体通道,并由媒体服务器完成流的接收、转封装与分发。
本文结合 完整 Mermaid 时序图、真实 SIP 信令日志、Go 语言实现代码,系统性梳理从“前端点击播放”到“用户看到画面”的全链路流程。
整体交互流程
以下为平台拉取设备实时流的完整时序:
sequenceDiagram
autonumber
participant 前端 as 📺 前端
participant GBS as 🎯 GBS(信令服务器)
participant SMS as 📹 Media Server(SMS)
participant 设备 as 📹 设备(GB28181)
rect rgb(230, 240, 255)
rect rgb(230, 240, 180)
Note over 设备: 配置信令服务器信息
设备 ->> GBS: 发起注册
GBS ->> 设备: 获取通道信息(catalog)
设备 ->> GBS: 响应通道信息(catalog)
end
rect rgb(230, 190, 180)
前端 ->> GBS: 播放请求
Note over GBS: 生成播放地址<br>${protocol}://${Media Server IP}/live/${stream name}.${extension}<br>例:ws://192.168.50.87:11005/live/stream_34020000001320000104_34020000001320000105_play.flv
GBS -->> 前端: 返回播放信息
end
Note over GBS, 设备: 开始拉流(异步)
rect rgba(130, 190, 180, 0.2)
Note over GBS: 如果是回放需要先关闭流
GBS -->> GBS: processer
Note over GBS: 生成流名称<br>流名称生成规则通常以devicUniqueId+channelUniqueId+playType作为唯一标识
GBS -->> GBS: processer
GBS -->> GBS: 播放拉流请求控制,防止重复拉流
Note over GBS: 检测流状态,输入型pub不存在需要先停止流
GBS ->> SMS: 分配端口
rect rgba(130, 190, 180, 0.2)
GBS -->> SMS: 发送start_rtp_pub请求
SMS -->> GBS: SMS应答
end
rect rgba(130, 190, 180, 0.2)
GBS -->> 设备: 发送Invite请求
设备 -->> GBS: 设备Invite应答
end
Note over GBS, 设备: ack请求需要使用Invite Response中的内容
GBS -->> 设备: 发送ACK请求
GBS -->> GBS: 解析sdp(Invite Response)
Note over GBS, SMS: 使用sdp中的ip和port作为SMS交互peer_port, peer_ip值
GBS -->> SMS: 发送ack_rtp_pub请求
end
end
关键点
- invite
- from: ip/port须填写信令服务器的地址信息
- to: ip/port须填写设备的地址信息
- Sip Request
- 在不使用Request Cache的情况下, invite和ack中的信息需要保持一致
- Call-ID, Via需要保持一致
- 严格参考国标文档, 如果有一个值填写错误将不能成功播放
- 注意区分发送/接收Invite中sdp信息中的差异, 由设备/信令服务器发出的sdp信息中字段值
- Origin ("o=")
- 信令服务器请求设备 通道id, IP/Port = SMS(告诉设备往哪个地址推流)
- 设备请求信令服务器 设备id, IP/Port = 设备地址(peer_ip,peer_port,告诉流媒体设备端推送地址)
- Connection ("c=")
- 信令服务器请求设备Address = SMS IP(推流的流媒体地址)
- 设备请求信令服务器Address = 设备 IP(设备端推流地址)
- Origin ("o=")
invite请求/响应
# 请求
[GBS] 2026-02-28 10:20:08 [UDP][192.168.50.87:11008]>>>>>>[192.168.50.104:5060]>>>>>>
INVITE sip:34020000001320000105@192.168.50.104:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.50.87:11008;rport=11008;branch=z9hG4bK2987171291
From: <sip:31010000042220000002@192.168.50.87:11008>;tag=811400952
To: <sip:34020000001320000105@192.168.50.104:5060>
Call-ID: 7994646177
User-Agent: SkeyevssSevVss 192.168.50.87
CSeq: 2 INVITE
Max-Forwards: 70
Content-Type: APPLICATION/SDP
Contact: <sip:31010000042220000002@192.168.50.87:11008>
Subject: 34020000001320000105:0200000105,31010000042220000002:0
Content-Length: 314
v=0
o=34020000001320000105 0 0 IN IP4 192.168.50.87
s=Play
u=34020000001320000105:0
c=IN IP4 192.168.50.87
t=0 0
m=video 30000 TCP/RTP/AVP 96 97 98 99
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:97 MPEG4/90000
a=rtpmap:98 H264/90000
a=rtpmap:99 H265/90000
a=setup:passive
a=connection:new
y=0200000105
# 响应
[GBS] 2026-02-28 10:20:08 [UDP][192.168.50.87:11008]<<<<<<[192.168.50.104:5060]<<<<<<
SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.50.87:11008;rport=11008;branch=z9hG4bK2987171291
From: <sip:31010000042220000002@192.168.50.87:11008>;tag=811400952
To: <sip:34020000001320000105@192.168.50.104:5060>;tag=1026846081
Call-ID: 7994646177
CSeq: 2 INVITE
Contact: <sip:34020000001320000104@192.168.50.104:5060>
Content-Type: application/sdp
User-Agent: IP Camera
Content-Length: 209
v=0
o=34020000001320000104 2463 2463 IN IP4 192.168.50.104
s=Play
c=IN IP4 192.168.50.104
t=0 0
m=video 15060 TCP/RTP/AVP 96
a=setup:active
a=sendonly
a=rtpmap:96 PS/90000
a=filesize:0
y=0200000105
Ack
[GBS] 2026-02-26 10:05:37 [UDP][192.168.50.87:11008]>>>>>>[192.168.50.104:5060]>>>>>>
ACK sip:34020000001320000105@192.168.50.104:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.50.87:11008;rport=11008;branch=z9hG4bK4389919483
From: <sip:31010000042220000002@192.168.50.87:11008>;tag=193700157
To: <sip:34020000001320000105@192.168.50.104:5060>;tag=385688899
Call-ID: 9367855484
User-Agent: SkeyevssSevVss 192.168.50.87
CSeq: 4 ACK
Max-Forwards: 70
Contact: <sip:31010000042220000002@192.168.50.87:11008>
Content-Length: 0
发送Invite请求
func (l *GBSSender) makeHeader(Type headerType) sip.Header {
return l.doMakeHeader(Type, nil)
}
func (l *GBSSender) makeHeaderWithBody(Type headerType, body string) sip.Header {
return l.doMakeHeader(Type, body)
}
func (l *GBSSender) makeHeaderWith(Type headerType, body interface{}) sip.Header {
return l.doMakeHeader(Type, body)
}
func (l *GBSSender) doMakeHeader(Type headerType, data interface{}) sip.Header {
switch Type {
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),
Params: sip.NewParams().
Add("rport", sip.String{Str: strconv.Itoa(int(port))}).
Add("branch", sip.String{Str: "z9hG4bK" + functions.RandWithString("0123456789", 10)}),
},
}
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()
case headerTypeTo:
return l.toAddress().AsToHeader()
case headerTypeToWith:
var toHeader = l.toAddress().AsToHeader()
toHeader.Address.SetUser(sip.String{Str: data.(string)})
return toHeader
case headerTypeCallId:
var callId = sip.CallID(functions.RandWithString("0123456789", 10))
return &callId
case headerTypeCallIdWith:
var callId = data.(sip.CallID)
return &callId
case headerTypeUserAgent:
var userAgent = sip.UserAgentHeader(MakeCascadeUserAgent(l.svcCtx.Config.Name, l.svcCtx.Config.InternalIp))
return &userAgent
case headerTypeMessageCSEq:
var csEq = sip.CSeq{
SeqNo: l.SN(l.deviceUniqueId),
MethodName: sip.MESSAGE,
}
if data != nil {
if v, ok := data.(sip.RequestMethod); ok {
csEq = sip.CSeq{
SeqNo: l.SN(l.deviceUniqueId),
MethodName: v,
}
}
}
return &csEq
case headerTypeMaxForwards:
var maxForwards = sip.MaxForwards(70)
return &maxForwards
case headerTypeContentType:
return &sip.GenericHeader{
HeaderName: HeaderContentType,
Contents: "Application/MANSCDP+xml",
}
case headerTypeContentTypeSDP:
return &sip.GenericHeader{
HeaderName: HeaderContentType,
Contents: "APPLICATION/SDP",
}
case headerTypeContentTypeMANSRTSP:
return &sip.GenericHeader{
HeaderName: HeaderContentType,
Contents: "Application/MANSRTSP",
}
case headerTypeContentLength:
return &sip.GenericHeader{
HeaderName: HeaderContentLength,
Contents: strconv.Itoa(len(data.(string))),
}
case headerTypeExpire:
return &sip.GenericHeader{
HeaderName: "Expires", Contents: "3900",
}
case headerTypeContact:
var contact = &sip.ContactHeader{DisplayName: l.req.DeviceAddr.DisplayName}
if l.req.DeviceAddr.Uri != nil {
contact.Address = l.req.DeviceAddr.Uri.Clone()
}
return 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),
},
}
)
return contact
case headerTypeEventPresence:
return &sip.GenericHeader{HeaderName: "Event", Contents: "presence"}
case headerTypeEventCatalog:
return &sip.GenericHeader{
HeaderName: "Event", Contents: fmt.Sprintf("Catalog;id=%s", functions.RandWithString("0123456789", 9)),
}
case headerTypeSubject:
if data == nil {
return nil
}
v, ok := data.(string)
if !ok {
return nil
}
return &sip.GenericHeader{HeaderName: "Subject", Contents: v}
default:
return nil
}
}
func (l *GBSSender) VideoLiveInvite(data *types.SipVideoLiveInviteMessage) (sip.Request, sip.Response, error) {
var (
headers = []sip.Header{
l.makeHeader(headerTypeVia),
l.makeHeader(headerTypeFrom),
l.makeHeader(headerTypeTo),
l.makeHeader(headerTypeCallId),
l.makeHeader(headerTypeUserAgent),
l.makeHeaderWith(headerTypeMessageCSEq, sip.INVITE),
l.makeHeader(headerTypeMaxForwards),
l.makeHeader(headerTypeContentTypeSDP),
l.makeHeader(headerTypeContactCurrent),
}
ssrc = strings.TrimSpace(data.ChannelUniqueId[3:8] + data.ChannelUniqueId[16:20])
playFlag = 0
isPlayback = data.StartAt != "" && data.EndAt != "" && data.PlayType == stream.PlayTypePlayback
)
if data.PlayType == stream.PlayTypePlayback {
playFlag = 1
atomic.AddInt64(&ssrcCounter, 1)
ssrc = strconv.FormatInt(ssrcCounter, 10)
}
if isPlayback {
headers = append(headers, l.makeHeaderWithBody(headerTypeSubject, fmt.Sprintf("%s:%d%s,%s:%s", data.ChannelUniqueId, playFlag, ssrc, l.svcCtx.Config.Sip.ID, ssrc)))
} else {
headers = append(headers, l.makeHeaderWithBody(headerTypeSubject, fmt.Sprintf("%s:%d%s,%s:0", data.ChannelUniqueId, playFlag, ssrc, l.svcCtx.Config.Sip.ID)))
}
var proto = "TCP/RTP/AVP"
if data.TransportProtocol.MediaProtocolMode == 0 {
proto = "RTP/AVP"
}
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,
Formats: []*sdp.Format{
{Payload: 96, Name: "PS", ClockRate: 90000},
{Payload: 97, Name: "MPEG4", ClockRate: 90000},
{Payload: 98, Name: "H264", ClockRate: 90000},
{Payload: 99, Name: "H265", ClockRate: 90000},
},
SSRC: fmt.Sprintf("%d%s", playFlag, ssrc),
},
},
URI: fmt.Sprintf("%s:0", data.ChannelUniqueId),
}
if data.TransportProtocol.MediaProtocolMode == 1 {
sdpInfo.Media[0].Attributes = sdp.Attributes{
sdp.NewAttr("setup", data.TransportProtocol.MediaTransMode),
sdp.NewAttr("connection", "new"),
}
}
if isPlayback {
startAt, err := time.ParseInLocation("2006-01-02 15:04:05", data.StartAt, time.Local)
if err != nil {
return nil, nil, err
}
endAt, err := time.ParseInLocation("2006-01-02 15:04:05", data.EndAt, time.Local)
if err != nil {
return nil, nil, err
}
sdpInfo.Name = "Playback"
sdpInfo.Timing = &sdp.Timing{
Start: startAt,
Stop: endAt,
}
if data.Download {
sdpInfo.Name = "Download"
if len(sdpInfo.Media) > 0 && len(sdpInfo.Media[0].Attributes) > 0 {
sdpInfo.Media[0].Attributes = append(
sdpInfo.Media[0].Attributes,
// sdp.NewAttr("downloadspeed", fmt.Sprintf("%d", data.Speed)),
sdp.NewAttr("downloadspeed", "4"),
)
}
}
}
if data.TransportProtocol.BitstreamIndex > 0 {
if v, ok := devices.VBitstreamIndexes[data.TransportProtocol.BitstreamIndex]; ok {
sdpInfo.Media[0].Attributes = append(
sdpInfo.Media[0].Attributes,
sdp.NewAttr(v.Key, v.Value),
)
}
}
var body = sdpInfo.String()
headers = append(headers, l.makeHeaderWithBody(headerTypeContentLength, body))
var request = l.makeRequest(sip.INVITE, headers, body)
response, err := l.Send(request)
if err != nil {
return nil, nil, err
}
return request, response, nil
}