GB28181
支持GB28181是正确的事情,可能也是困难的事情,因为困难所以有趣。
重要说明:SRS 5.0已经是beta或更稳定的版本,推荐使用SRS 5.0的GB,而不要使用4.0的GB,因为虽然4.0是稳定发布版本,但是4.0的GB是实验性的和不稳定的。
研发的详细过程请参考#3176。
Usage
首先,编译和启动SRS,请确认版本为5.0.74+
:
./configure --gb28181=on
make
./objs/srs -c conf/gb28181.conf
Note: 如果你是公网服务器,一定要配置对Candidate,请参考Candidate的说明。
然后,在摄像头配置中,选择AAC编码,然后在平台中配置SIP服务器为SRS,如下图所示:
- 必须是
AAC
编码,在音频编码中,选择AAC
,采样率44100HZ
。 - 必须是
GB-2016
标准,否则不支持TCP
,在协议版本中选择GB/T28181-2016
。 - 必须是
TCP
协议,不支持UDP
,在传输协议中选择TCP
,并使用GB-2016
标准。
摄像头注册后,SRS会自动邀请摄像头推流,可以打开下面的链接播放:
- http://localhost:8080/live/34020000001320000001.flv
- http://localhost:8080/live/34020000001320000001.m3u8
- webrtc://localhost/live/34020000001320000001
Note: 请把流名称换成你的设备名称,然后点播放。
Candidate
如果服务器IP不是内网IP,比如部署在公网,则SRS无法获取自己的 出口IP,需要配置:
stream_caster {
enabled on;
caster gb28181;
listen 9000;
sip {
enabled on;
listen 5060;
candidate a.b.c.d;
}
}
Note: 请将
stream_caster.sip.candidate
换成摄像头能访问到你的服务器的IP,不管是内网还是公网IP,摄像头能访问到就可以。
GB的Candidate定义和WebRTC: Candidate概念上一致,都是需要暴露一个客户端能访问的IP地址,在SDP中传递给客户端。比如:
- 在SRS配置中设置
stream_caster.sip.candidate
,SRS启动会读取这个配置,比如192.168.1.100
。 - GB设备通过SIP注册到SRS,SRS发起INVITE消息,消息的Body就是SDP,SDP会指定这个IP地址,比如
IN IP4 192.168.1.100
。 - GB设备连接这个IP地址
tcp://192.168.1.100:9000
,并发起媒体请求。
Note: 媒体的端口是配置在
stream_caster.listen
中的,目前只支持TCP端口。
这个CANDIDATE
就是媒体服务器的IP,它和SIP的服务器地址可以是不同的,SIP服务器地址是在Usage中配置在客户端的。
Note: 由于GB的SIP协议,在REGISTER时To字段并没有带服务器的地址,所以导致服务器无法从SIP中发现自己的地址,只能依靠服务器配置。
当然,如果网卡配置了客户端可以访问的地址,可以把CANDIDATE
配置为*
,让SRS自己发现。
Latency
与普遍认知相反,安全摄像头并非天生就是低延迟系统。它们的主要目的是长期存储。在平移-倾斜-缩放(PTZ)场景中,可能存在一定的延迟要求, 但这并不是我们通常所说的低延迟。PTZ摄像头 的延迟通常在1秒左右,这被认为是可以接受的,而实时通信(RTC)中的低延迟通常在200ms左右。
延迟不仅与服务器有关,还涉及到整个链路。关于详细的延迟优化,请参考Low Latency。 我使用默认配置测试了海康威视摄像头的延迟,如下所示:
- 流类型:
子流
- 视频类型:
复合流
- 分辨率:
640x480
- 码率类型:
可变码率
- 图像质量:
中等
- 视频帧率:
25
fps - 码率限制:
1024
Kbps - 视频编码:
H.264
- 编码复杂度:
中等
- I帧间隔:
50
- SVC:
禁用
- 平台接入协议:
GB/T28181-2016
- 平台传输协议:
TCP
,即通过GB/TCP将流推送到SRS - 观看:WebRTC(UDP)
- SRS版本:
v5.0.100
- 摄像头和服务器:内网
- 延迟:358ms
Features
目前SRS支持的GB的功能清单:
- 摄像头通过SIP注册。srs-gb28181支持。SRS 5.0 支持。
- 自动邀请摄像头推流。srs-gb28181支持。SRS 5.0 支持。
- GB/2016转RTMP协议。srs-gb28181支持。SRS 5.0 支持。
- 基于TCP的SIP信令。srs-gb28181支持。SRS 5.0 支持。
- TCP单端口传输媒体。srs-gb28181支持。SRS 5.0 支持。
目前还没有支持的GB功能:
- 基于UDP的SIP信令。srs-gb28181支持。SRS 5.0 不支持。
- UDP单端口传输媒体。srs-gb28181支持。SRS 5.0 不支持。
- GB/2011转RTMP协议。srs-gb28181支持。SRS 5.0 不支持。
- UDP/TCP多端口传输媒体。srs-gb28181支持。SRS 5.0 不支持。
- HTTP API查询GB流。srs-gb28181支持。SRS 5.0 不支持。
- HTTP API云台摄像头。srs-gb28181支持。SRS 5.0 不支持。
- Web管理页面。srs-gb28181支持。SRS 5.0 不支持。
- GB下级服务器。srs-gb28181不支持。SRS 5.0 不支持。
- GB语音对讲。srs-gb28181不支持。SRS 5.0 不支持。
- GB回看。srs-gb28181不支持。SRS 5.0 不支持。
- GB加密传输。srs-gb28181不支持。SRS 5.0 不支持。
希望大家降低期望,GB的坑太难填了,希望不要期待SRS能做多好。
Protocols
GB相关的协议如下:
- RFC3261: SIP: Session Initiation Protocol
- RFC4566: SDP: Session Description Protocol
- RFC4571: RTP & RTCP over Connection-Oriented Transport
- GB28181-2016: 公共安全视频监控联网系统信息传输、交换、控制技术要求
- ISO13818-1-2000: MPEGPS(Program Stream), PS媒体流规范。
SIP Parser
C++没有特别好的SIP库,这也是SIP处理不稳定的一个原因。
不过发现SIP协议和HTTP协议结构非常一致,因此SRS采用http-parser解析SIP,这个库是nodejs维护的,之前好像是NGINX中扒出来的,所以稳定性还是非常高的。
当然用HTTP解析SIP,需要有些修改,主要是以下修改:
- Method:需要新增几个方法,比如
REGISTER
、INVITE
、ACK
、MESSAGE
和BYE
,这是GB常用的几个消息。 - RequestLine:解析path时需要修改,SIP是
sip:xxx
格式,会被认为是HTTP完整URL格式导致解析失败。 - ResponseLine:生成Response时需要修改,主要是协议头,从
HTTP/1.1
改成SIP.2.0
。
基本上改变非常小,所以协议稳定性是可以保障,可以算是解决了一个难题。
SIP和HTTP不同的是,在同一个TCP通道中,并不一定就是一个Request对应一个Response,比如INVITE之后,可能会有100和200两个响应,而SRS也不固定就是Server,也有可能是Client。而这些情况,http-parser可以设置为BOTH方式,这样可以解析出Request和Response:
SrsHttpParser* parser = new SrsHttpParser();
SrsAutoFree(SrsHttpParser, parser);
// We might get SIP request or response message.
if ((err = parser->initialize(HTTP_BOTH)) != srs_success) {
return srs_error_wrap(err, "init parser");
}
Note: 从HTTP消息来看,并没有规定只能一个Request对应一个Response,因此这个也不会带来额外问题。
在实际解析中,发现有时候发送的头有空格,比如:
Content-Length: 142\r\n
这实际上是符合规范的,但如果手动解析可能会有问题,而HTTP-Parser能正确处理这种情况。
REGISTER
GB的注册流程:
- 在设备设置好SIP服务器为SRS。
- 设备发送SIP格式的REGISTER消息。
- SRS回应200/OK,注册成功。
GB的心跳:
- 设备后面会不断发送MESSAGE作为心跳消息。
- SRS回应200/OK,心跳成功。
SRS若重启后,由于没有保存任何状态,所以收到的可能是设备的MESSAGE消息,而没有REGISTER消息,所以希望设备能重新注册。向各位同学以及SIP和GB的专家请教后,重新注册的可能方法包括:
- 不回应MESSAGE消息,一般3次心跳超时(在设备配置上有设置)。验证发现,海康设备心跳周期默认60秒,所以大概在3分钟左右会重新注册。
- 对MESSAGE回应403或者其他消息。验证发现,效果和不回应一样,设备并不会特别处理。
- 给设备发送重启指令,参考
A.2.3 控制命令
,远程启动是<TeleBoot>Boot</TeleBoot>
,尝试重启设备后会重新注册。这个还没验证。 - 回应REGISTER消息时,将EXPIRE设置短一些,缩短注册的间隔,比如改成30秒。验证发现,尽管设置为30秒,但还是会在一个心跳时间才会重新注册,也就是60秒。
- 在设备的配置上,将心跳周期改短一些,默认60秒,最小是5秒,这样超时会更快。验证发现,心跳改5秒,最短可以26秒左右重新注册。
- 加一层SIP Proxy,让Proxy来保存相关的信息,将状态转移到Proxy。这个方案应该可行,不过SRS不太合适,引入额外组件会让开源很复杂,大家自己的实现中可以尝试。
- 重启前发消息。这个方案在SRS Gracefully Quit时有效,但有时候会
kill -9
或者系统OOM,不会给程序机会清理,所以这个不能适应所有场景。不过在主动升级时,一般会用Gracefully Quit,这时可以有机会处理这个问题,大家可以尝试。
总之,是没有特别可靠的办法能让摄像头立刻重新注册,SRS必须在逻辑上处理这个问题:SRS启动或重启后,摄像头还在已经注册,甚至在传输流的状态。
Note: 由于很多问题都是持续长时间运行,而系统的某一方重启了,导致状态不一致,引起各种问题。因此,在SRS重启或者启动时,若发现有摄像头是在注册或传输流的状态,那么应该尝试让摄像头重新走一次流程,比如重新注册和重新推流,这样让双方的状态一致,可靠性会 更高。
Note: 验证发现,重新注册,对正在传输的媒体流不影响。设备会探测端口可达性,如果TCP断开,或者UDP端口不可达,则会停止流传输。
TCP or UDP
在使用TCP或UDP协议上,我们选择先支持TCP协议,包括SIP信令和PS媒体。
根据SIP协议的规定,TCP是必须要支持的,也是RFC3261比RFC2543一个重要的更新,参考RFC3261: Transport。
至于媒体协议,GB由于使用了PS格式,其实PS一般是用于存储格式,而TS是网络传输格式,或者说TS考虑了更多的网络传输问题,而PS则更多假设像磁盘读写文件一样可靠,因此,PS基于TCP传输也会更加简单。
GB 2016中对于TCP的描述在附录L
,即基于TCP协议的视音频媒体传输
:
实时视频点播、历史视频回放与下载的TCP媒体传输应支持基于RTP封装的视音频PS流,封装格式参照IETF RFC 4571。
在实际应用中,大部分也是使用TCP,而不是用UDP,特别在公网上UDP会有丢包,而GB没有设计重传或FEC。使用TCP的好处:
- UDP由于无状态,在服务器重启时,设备感知不到服务重启,可能还能继续传输数据,导致两边状态不同步,长久持续这样可能会导致问题,比如设备会提示请求超过上限。
- GB的信令和媒体分离,如果使用TCP则可以很好的同步状态,比如信令可用媒体不可用或断开,媒体可用信令不可用,这些最终都反应到连接的断开。具体请参考Protocol Notes。
- 服务器重启后,可以使用缩短REGISTER的Expires,缩短心跳间隔,让设备重新注册,重新进入推流状态。服务器重启后,设备可以快速感知到媒体链路断开。
- 传输过程 中,若出现网络抖动导致链接断开,服务器和设备都可以很快感知到,进入异常处理流程。
因此,SRS先支持TCP,而不支持UDP。也就是先支持GB28181 2016,而不是支持GB28181 2011。
Note: 需要显式开启GB28181-2016,并开启TCP协议才可以。
Protocol Notes
SIP协议上特别需要注意的地方:
- Via的branch必须是
z9hG4bK
开头,参考Via的说明。 - INVITE的200(OK)的ACK消息,ACK的Via的branch必须是新的,ACK并不是INVITE的transaction,参考Via和Example。
- INVITE的Contact是自己的地址,而不是GB设备的,也就是Contact应该由From生成而不是To,参考Contact和Example。
- INVITE的Subject,定义为
媒体流发送者ID:发送方媒体流序列号,媒体流接收者ID:接收方媒体流序列号
,参考附录K。对于s=Play
实时观看的场景,接收方媒体流序列号(SSRC)其实没有定义;根据各位同学反馈,一般这个字段填0。
SDP协议上特别注意的地方:
- y字段: 为十进制整数字符串, 表示SSRC值。格式如下:
dddddddddd
。其中, 第1位为历史或实时媒体流的标识位, 0为实时, 1为历史;第2位至第6位取20位SIP监控域ID之中的4到8位作为域标识, 例如13010000002000000001
中取数字10000
; 第7位至第10位作为域内媒体流标识, 是一个与当前域内产生的媒体流SSRC值后4位不重复的四位十进制整数。
Note: SDP中的
y=
字段,是GB扩展的字段,在WebRTC中是用a=ssrc:xxxx
表达的SSRC。
信令和媒体配合:
- 信令注册、INVITE、TRYING、200、ACK后,媒体开始传输。参考 gb-media-ps-normal.pcapng.zip
- 媒体正常传输过程中,信令重新注册,不影响媒体,继续正常传输。 参考 gb-media-ps-sip-register-loop.pcapng.zip
- 信令正常完成INVITE,媒体TCP端口若不打开,设备尝试连接一次后放弃。参考 gb-media-disabled-sip-ok.pcapng.zip
- 媒体正常传输过程中,信令断开,一定时间后,媒体断开。参考 gb-media-ps-sip-disconnect.pcapng.zip
- 媒体正常传输过程中,TCP连接断开,客户端不会重试。参考 gb-media-disconnect-sip-ok.pcapng.zip
媒体协议:
- 解析媒体流时,可能会出现各种错误,此时会丢弃整个pack的数据,直到下一个pack到来(
00 00 01 ba
)。其中包括RTP解析失败,非法的PS头(非00 00 01
开头),部分PES头(比如在前一个TCP包的尾部),甚至还有RFC4571的包解析失败(头两个字节代表的长度信息是0)。 - SRS支持恢复模式,遇到解析摄像头的包失败会进入恢复模式,但有时候也会出现无法恢复的情况,因此会限制每次最大的恢复次数,如果连续多个包还不能恢复,那就断开媒体连接,进入信令重新INVITE的过程。若包长度异常,很大概率是无法恢复,则关闭恢复模式,直接进入重新INVITE流程。
- 媒体使用MPEGPS流,其中length为16位,也就是PES最大长度为64KB。PS是对超过64KB的帧直接分包,分成多个PES,按照时间戳组合;另外,一个pack中只有一个Video,可能会有多个Audio,因此Audio每帧不超过64KB。示例如下所示:
媒体PS组包,超过64KB的情况:
PS: New pack header clock=2454808848, rate=159953
PS: New system header rate_bound=159953, video_bound=1, audio_bound=1
PS: Got message Video, dts=2454808848, seq=22204, base=2454808848 payload=29B, 0, 0, 0, 0x1, 0x67, 0x4d, 0, 0x32
PS: Got message Video, dts=0, seq=22204, base=2454808848 payload=8B, 0, 0, 0, 0x1, 0x68, 0xee, 0x3c, 0x80
PS: Got message Video, dts=0, seq=22204, base=2454808848 payload=9B, 0, 0, 0, 0x1, 0x6, 0xe5, 0x1, 0x2b
PS: Got message Video, dts=0, seq=22250, base=2454808848 payload=65471B, 0, 0, 0, 0x1, 0x65, 0xb8, 0, 0
PS: Got message Video, dts=0, seq=22252, base=2454808848 payload=2112B, 0x48, 0x4c, 0xf2, 0x94, 0xaa, 0xbc, 0xed, 0x3d
PS: Got message Audio, dts=2454812268, seq=22253, base=2454808848 payload=99B, 0xff, 0xf9, 0x50, 0x40, 0xc, 0x7f, 0xfc, 0x1
PS: Got message Audio, dts=2454814338, seq=22254, base=2454808848 payload=96B, 0xff, 0xf9, 0x50, 0x40, 0xc, 0x1f, 0xfc, 0x1
PS: New pack header clock=2454812448, rate=159953
PS: Got message Video, dts=2454812448, seq=22283, base=2454812448 payload=39457B, 0, 0, 0, 0x1, 0x61, 0xe0, 0x8, 0xbf
PS: Got message Audio, dts=2454816498, seq=22284, base=2454812448 payload=101B, 0xff, 0xf9, 0x50, 0x40, 0xc, 0xbf, 0xfc, 0x1
PS: Got message Audio, dts=2454818568, seq=22285, base=2454812448 payload=107B, 0xff, 0xf9, 0x50, 0x40, 0xd, 0x7f, 0xfc, 0x1
Note: 这两有两个pack,每个pack只有一个Video帧(不算编码头),每个都有两个Audio包。 Note: 第一个pack,前三个Video(Seq=22204),是编解码信息,一般在I帧前面都是编码头,SPS/PPS等信息。 Note: 第一个pack,后两个Video(Seq=22250/22252)实际上就是一个关键帧,第一个是
00 00 01
开头,第二个直接就是接续的视频数据;第一个超过64KB,所以分成了两个。 Note: 第一个pack,最后两个Audio消息,它们时间戳是不同的。 Note: 第二个pack,只有一个Video,没超过64KB,而且没有system header和PSM,所以一般不是关键帧(具体以NALU解析为准)。 Note: 第二个pack,后面两个是Audio消息,时间戳也不同。 Note: 两个pack的Video间隔,是2454812448-2454808848=3600
,也就是40ms,也就是视频FPS=25。而Audio之间的间隔,是2454810198-2454808128=2070
,也就是23ms。
Wireshark
Wireshark默认就能解析GB的SIP的包,5060端口认为是SIP的默认端口。而GB媒体则需要操作下,这小节总结下如何用Wireshark解析媒体包。
SRS使用TCP传输媒体,所以格式是RFC4571: RTP & RTCP over Connection-Oriented Transport,就是前两个字节是长度,后面是RTP包。
Note: Wireshark支持RFC4571,它的Dissecotr是
rtp.rfc4571
。
有两种方法,一种直接打开包后,输入过滤tcp.port==9000
,然后右键包,选择Decode as > RTP
,就可以看到解析成了RFC4517,如下图所示:
还有一种方法,直接加载SRS的research/wireshark/gb28181.lua插件,将TCP/9000数据解析为RFC4571格式,执行如下命令:
cd ~/git/srs/trunk/research/wireshark
mkdir -p ~/.local/lib/wireshark/plugins
ln -sf $(pwd)/gb28181.lua ~/.local/lib/wireshark/plugins/gb28181.lua
Note: Wireshark的插件目录,不同平台会不同,请百度下在哪里,直接把插件拷贝进去也可以。
解析成功后,直接过滤rtp
包,可以看到GB的媒体数据,如下图所示:
Note: 注意RTP的Payload就是MPEG-PS,开头是
00 00 01 BA
的标识符,不过Wireshark不支持PS流解析。
工具准备好了,分析起来也会更方便。
Lazy Sweep
GB存在和Source清理一样的问题。在GB中,存在SIP连接协程,媒体连接协程,会话协程等多个协程,这些协程之间会互相引用对象,而它们的生命周期是不一致的。
比如:SIP连接,需要持有会话对象的指针,当设备连接到SRS时,需要更新会话协程的SIP连接对象,这样会话需要发送信令消息,就可以走最新的SIP连接发送。
比如:媒体连接,收到媒体PS pack时,需要通知会话协程处理,转成RTMP流。媒体连接断开时,需要通知会话协程,会话协程会发送BYE和重新INVITE,通知设备重新推流。
比如:会话对象,有自己的生命周期,简单设计就是和Source一样永远不清理,这样它生命周期就会比SIP和 媒体协程活得更长,这样它们引用会话对象时就是安全的,但这样就会有内存不释放的问题。同样,SIP连接一定需要清理,所以会话对象就可能会持有野指针问题。
Source清理的问题,本质上是多个协程之间生命周期不同步,所以如果释放Source后可能有些协程活得比Source更久,就可能出现野指针引用。详细请查看#413的描述。
而这些问题的解决方案都是Lazy Sweep,也就是互相持有的不是裸指针,而是Wrapper指针(有点像C++ 11的智能指针只是没有什么智能可言),因为Wrapper释放后Resource还是可用的,其他Wrapper对象还是可以使用Resource,我们在释放Resource时,等所有Wrapper都释放后再安全释放,也就是Lazy Sweep。
先在SRS 5.0 GB上实现这个机制,估计在6.0就可以比较容易实现Source的释放了。
比如有两个资源,互相依赖,定义如下:
class SrsLazyGbSipTcpConn : public ISrsLazyResource {
private:
SrsLazyGbSessionWrapper* session_;
};
class SrsLazyGbSession : public ISrsLazyResource {
private:
SrsLazyGbSipTcpConnWrapper* sip_;
};
它们对应的Wrapper定义如下,使用宏定义会很简单:
class SrsLazyGbSipTcpConnWrapper : public ISrsResource, public ISrsGbSipConnWrapper {
SRS_LAZY_WRAPPER_GENERATOR(SrsLazyGbSipTcpConn, SrsLazyGbSipTcpConnWrapper, SrsLazyGbSipTcpConn);
};
class SrsLazyGbSessionWrapper : public ISrsResource {
SRS_LAZY_WRAPPER_GENERATOR(SrsLazyGbSession, SrsLazyGbSessionWrapper, SrsLazyGbSession);
};
比如sip对象创建Session,放到全局对象管理manager里面,然后自己拷贝一份引用:
SrsLazyGbSessionWrapper* session = new SrsLazyGbSessionWrapper();
_srs_gb_manager->add_with_id(device, session);
session_ = session->copy();
这样在释放sip对象时,是直接释放的session的wrapper,而在sip对象生命周期中,session一直是可用的:
SrsLazyGbMediaTcpConn::~SrsLazyGbMediaTcpConn() {
srs_freep(session_);
}
srs_error_t SrsLazyGbSipTcpConn::cycle() {
session_->resource()->on_sip_dispose();
同样,session对象拥有的是sip对象的wrapper,是sip自己传过来的,session会持有一份拷贝:
void SrsLazyGbSession::on_sip_transport(SrsLazyGbSipTcpConnWrapper* sip) {
srs_freep(sip_);
sip_ = sip->copy();
}
这样在session释放时,也是直接释放sip的wrapper,在session生命周期中,sip一直都是可用的:
SrsLazyGbSession::~SrsLazyGbSession() {
srs_freep(sip_);
}
srs_error_t SrsLazyGbSession::cycle() {
sip_->resource()->interrupt();
srs_error_t SrsLazyGbSession::do_cycle() {
while (true) {
if (sip_->resource()->is_bye()) {
srs_error_t SrsLazyGbSession::drive_state() {
if (state_ == SrsGbSessionStateInit) {
if (sip_->resource()->is_registered()) {
而sip和session在释放自己的wrapper时,也不用关注谁在使用,只需要根据自己的创建选择对应的释放即可,比如一般使用manager管理对象,那么就应该使用manager释放对象:
srs_error_t SrsLazyGbSipTcpConn::cycle() {
// Note that we added wrapper to manager, so we must free the wrapper, not this connection.
SrsLazyGbSipTcpConnWrapper* wrapper = dynamic_cast<SrsLazyGbSipTcpConnWrapper*>(gc_creator_wrapper());
_srs_gb_manager->remove(wrapper);
srs_error_t SrsLazyGbSession::cycle() {
// Note that we added wrapper to manager, so we must free the wrapper, not this connection.
SrsLazyGbSessionWrapper* wrapper = dynamic_cast<SrsLazyGbSessionWrapper*>(gc_creator_wrapper());
_srs_gb_manager->remove(wrapper);
此外,wrapper对象就是普通的对象,可以直接被释放,也可以用manager释放,都是可以的。
这个方案和直接裸指针引用的差别,在于增加了一个wrapper对象,没有任何其他特别的地方,看起来只是加了一层函数而已,是最简单的方案。
Benchmark
GB缺乏工具链,基本上是空白的,而没有工具,就只能借助真实的摄像头测试,这基本上就是原始时代:
- 摄像头只能覆盖基本的正常流程,以及一些能操作的异常流程,无法覆盖SRS设计的状态机,会导致某些状态下异常。
- 摄像头的断开和重推流周期太慢,估计得15秒左右,而工具比如utest或benchmark,可以做到1秒就能重推,这样效率才能提高。
- 摄像头无法实现utest和回归测试,这次测试完是好的,但未来不知道什么时候就改坏了,这样出现问题后排查的效率就非常低。
和WebRTC一样,SRS也会完善GB的工具链,参考srs-bench,我们会基于Go的各种库实现GB的自动测试,也可以用作模拟摄像头。使用到的库包括:
- ghettovoice/gosip:SIP协议栈,这个库WebRTC段维伟也有贡献,模拟GB的SIP。
- gomedia/mpeg2:打包PS流,包括Pack头、System头、PSM包、音视频PES包等。
- pion/rtp:打包RTP头。由于GB的SDP不标准,所以没用pion的SDP解析,直接字符串查找SSRC即可。
- pion/h264reader: 读取h264格式的视频文件,在压测工具中,使用FFmpeg将FLV转成h264格式的视频文件,方便测试时分开测试音视频。
- go-oryx-lib/aac: 读取AAC格式的音频文件,在压测工具中,使用FFmpeg将FLV转成ogg/aac等音频文件格式,方便测试时分开测试音视频。
Go的库一致性比C++的高,当然风格也有差别,调试很方便,用作Benchmark工具是足够了。
使用方法,下载代码后编译,执行--help
可以看到参数和实例,注意依赖Go编译环境请先安装Go:
git clone -b feature/rtc https://gitee.com/ossrs/srs-bench.git
cd srs-bench
make && ./objs/srs_bench -sfu gb28181 --help
模拟一个摄像头推流:
./objs/srs_bench -sfu gb28181 -pr tcp://127.0.0.1:5060 -user 3402000000 -random 10 \
-server 34020000002000000001 -domain 3402000000 -sa avatar.aac \
-sv avatar.h264 -fps 25
Note: SRS使用user字段作为设备标识,转成RTMP也作为流名称,压测工具支持随机10位数字的user,通过
random
指定,这样可以每次模拟不同的设备。如果希望模拟一台固定的设备,不指定random
,而指定完整的user即可。
Note: 需要先启动本机SRS。压测工具自带了测试样本
avatar.h264
和avatar.aac
,如果需要其他的测试样本,可以用FFmpeg生成。
同样,SRS的回归测试,也会执行GB的回归测试,每次提交都会检查是否GB正常,也可以手动执行回归测试:
cd srs-bench
go test ./gb28181 -mod=vendor -v
Note: 测试前需要先启动SRS服务器,参考前面压测的说明。
Go最厉害的是这些控制机制,覆盖得非常全面,比如:
- 启动三个协程,必须等三个协程结束后才能退出,并判断错误结果,决定测试是否成功。
- 推流和播放协程,必须 等待主协程初始化完毕才能启动。
- 播放协程,必须等待推流建联后才能建联。
- 所有协程,都不能超过测试用例的超时时间,比如5秒。
- 若有协程异常,应该立刻结束,比如播放异常,推流就算正常也应该结束。
- 若所有正常,也应该在一定包数之后结束,比如收发100个包,这样尽快可以跑完测试,比如100ms,而不是每个必须5秒(会导致整体测试时间太长)。
这些全都是控制机制,Go用了select+chan
、WaitGroup
、Context
三个基础组件就全部支持了,不得不佩服Go这个设计还是非常非常牛逼的。
Commits
和GB相关的修改:
- HTTP: Support HTTP header in creating order. v5.0.68 GB的SIP头没有明确要求有序,不过倒是提过尽量优先,比如Via一般是放第一个。SIP使用HTTP协议栈解析,所以SIP的头有序,就改了HTTP支持头有序,按照添加的顺序,而不是默认的字母顺序。
- Kernel: Support lazy sweeping simple GC. v5.0.69 支持简单的延迟清理的GC,解决多个协程之间依赖对象的问题,未来Source清理也可以使用这个机制。在GB上这个问题非常严重,因为有多个协程和多个对象互相依赖,清理时特别容易出现问题。此外,WebRTC over TCP也有两个协程的交互,也可以采用这个机制。
- GB28181: Refine HTTP parser to support SIP. v5.0.70 使用http parser解析SIP协议栈,本质上SIP和HTTP在协议格式上基本一致,在RFC中也说明了这一点。详细参考SIP Parser中的说明。
- RTC: Refine SDP to support GB28181 SSRC spec. v5.0.71 使用RTC的SDP对象,解析和编码GB的SDP,主要是支持SSRC的格式
y=ssrc
的方式,以及一些不同的需要定义的字段。 - ST: Support set context id while thread running. v5.0.72 支持协程运行过程中改变ContextID,SIP和媒体线程运行后,从包中解析才知道对应的Session,然后将自己的ContextID设置为Session的,这样可以将日志全部打印到一个ContextID上,排查时可以查询这个ID即可。
- HTTP: Skip body and left message by upgrade. v5.0.73 解决HTTP Parser调用的问题,支持SIP这样可能多个Request或Response消息的情况,只解析头并且不解析剩下的数据,避免解析失败。
- GB28181: Support GB28181-2016 protocol. v5.0.74 GB的主要逻辑,大约3.5K行左右,其他是测试代码大约4K行,以及srs-bench依赖的Go的第三方库。
Thanks
特别感谢夏立新等同学,两年前让SRS支持了GB功能,经过这两年的积累,我们形成了GB的开源社区,了解了GB的应用场景,以及主要的发展方向。
经过这两年对GB的理解,我们也终于有信心把GB合并到SRS 5.0,除了夏立新和陈海博,其中有非常多的同学的贡献,很抱歉无法一一表达。
在合并GB进SRS 5.0过程中,对于其中的难点和疑点,也有很多同学给与了帮助,包括王冰洋、陈海博、沈巍、周小军、夏立新、杜金房、姚文佳、潘林林等等同学。