支持GB28181是正确的事情,可能也是困难的事情,因为困难所以有趣。
Introduction
在非常多朋友的努力下,SRS的GB功能不少,详细可以参考srs-gb28181[1]。由于GB和摄像头的复杂性,问题也是不少的,特别是稳定性问题,这也是为什么GB一直迟迟没有进SRS 5.0分支的原因。
现在SRS 5.0已经临近功能封版了,我们增加了几个大的功能和改进,最后一个功能就是在考虑是否支持GB。鉴于GB目前的稳定性表现,肯定不能完全合并过来,是否能有稳定性更高的合并办法?
如果减少功能,当然稳定性就会提升,所以SRS 5.0可能的合并方式,就是只合并一个最简单的GB的能力,我们就叫它PoC吧。我列一下我知道的GB的功能清单:
- 1. 摄像头通过SIP注册。srs-gb28181支持。PoC支持。
- 2. 自动邀请摄像头推流。srs-gb28181支持。PoC支持。
- 3. GB/2016转RTMP协议。srs-gb28181支持。PoC支持。
- 4. 基于TCP的SIP信令。srs-gb28181支持。PoC支持。
- 5. TCP单端口传输媒体。srs-gb28181支持。PoC支持。
- 6. 基于UDP的SIP信令。srs-gb28181支持。PoC不支持。
- 7. UDP单端口传输媒体。srs-gb28181支持。PoC不支持。
- 8. GB/2011转RTMP协议。srs-gb28181支持。PoC不支持。
- 9. UDP/TCP多端口传输媒体。srs-gb28181支持。PoC不支持。
- 10. HTTP API查询GB流。srs-gb28181支持。PoC不支持。
- 11. HTTP API云台摄像头。srs-gb28181支持。PoC不支持。
- 12. Web管理页面。srs-gb28181支持。PoC不支持。
- 13. GB下级服务器。srs-gb28181不支持。PoC不支持。
- 14. GB语音对讲。srs-gb28181不支持。PoC不支持。
- 15. GB回看。srs-gb28181不支持。PoC不支持。
- 16. GB加密传输。srs-gb28181不支持。PoC不支持。
Note: PoC就是SRS 5.0打算合并进来的GB的能力。srs-gb28181[2]就是SRS目前支持的GB的能力。
是否合并进5.0的条件,是需要评估下,是否能达到相对比较稳定的状态,以及能持续维护这个功能。从这两个角度说,尽量少的功能,都是对这两个目标有利的。
实际应用中,可能真正直接用SRS就上线GB项目基本上不可能,实际上也没有一个开源项目能做到,因为GB有非常多的业务定制,必须依赖深度定制才能上线。基于这个判断,SRS的GB和多线程类似,主要是提供基本的框架,方便大家定制,让大家在定制时能有一个相对比较稳定的基础。
特别说明的是,建议使用成熟的SIP库,比如Java写的jsip[3],而不要基于SRS的SIP实现。因为SIP有非常多的业务逻辑,C++写业务逻辑非常容易出问题。我们基于jsip实现了一个例子,供大家参考srs-sip[4]。
Note: SRS 5.0还是会实现一个SIP协议栈,但是只会实现非常非常基础的能力,未来也不会继续扩展它,主要是为了方便接入。
看到不断的有朋友在使用SRS做GB的项目,而大家一致反馈GB是个很难填的坑,希望SRS微薄的贡献,能让GB开发者日子过的稍微容易点点。
Note: 其实,除了GB协议,越来越多的摄像头开始支持RTMP推流,甚至SRT或者WebRTC推流。从未来看,GB协议也不一定就是唯一可用的协议,当然了人应该活在当下,既然有这么多开发者在用SRS做GB,那么SRS尽量把GB做好一些,也是应该做的。
最后,最重要的,希望大家降低期望,GB的坑太难填了,希望不要期待SRS能做多好。
Usage
首先,编译和启动SRS,请确认版本为5.0.74+
:
./configure --gb28181=on
make
./objs/srs -c conf/gb28181.conf
复制
然后,在摄像头配置中,选择AAC编码,然后在平台中配置SIP服务器为SRS,如下图所示:
;
;
- • 必须是
AAC
编码,在音频编码中,选择AAC
,采样率44100HZ
。 - • 必须是
GB-2016
标准,否则不支持TCP
,在协议版本中选择GB/T28181-2016
。 - • 必须是
TCP
协议,不支持UDP
,在传输协议中选择TCP
,并使用GB-2016
标准。
摄像头注册后,SRS会自动邀请摄像头推流,可以打开下面的链接播放:
- • http://localhost:8080/live/34020000001320000001.flv[5]
- • http://localhost:8080/live/34020000001320000001.m3u8[6]
- • webrtc://localhost/live/34020000001320000001[7]
Note: 请把流名称换成你的设备名称,然后点播放。
由于我家里设备不方便长时间推流测试,我最多推流了13小时还是正常的,欢迎大家用专门的测试环境和设备测一测,我们第一个小目标是不间断推流一个月。
Candidate
GB的Candidate定义和WebRTC: Candidate[8]概念上一致,都是需要暴露一个客户端能访问的IP地址,在SDP中传递给客户端。比如:
- 1. 在SRS配置中设置
stream_caster.sip.candidate
,SRS启动会读取这个配置,比如192.168.1.100
。 - 2. GB设备通过SIP注册到SRS,SRS发起INVITE消息,消息的Body就是SDP,SDP会指定这个IP地址,比如
IN IP4 192.168.1.100
。 - 3. GB设备连接这个IP地址
tcp://192.168.1.100:9000
,并发起媒体请求。
Note: 媒体的端口是配置在
stream_caster.listen
中的,目前只支持TCP端口。
这个CANDIDATE
就是媒体服务器的IP,它和SIP的服务器地址可以是不同的,SIP服务器地址是在Usage[9]中配置在客户端的。
Note: 由于GB的SIP协议,在REGISTER时To字段并没有带服务器的地址,所以导致服务器无法从SIP中发现自己的地址,只能依靠服务器配置。
当然,如果网卡配置了客户端可以访问的地址,可以把CANDIDATE
配置为*
,让SRS自己发现。
Protocols
GB相关的协议如下:
- • RFC3261: SIP: Session Initiation Protocol[10]
- • RFC4566: SDP: Session Description Protocol[11]
- • RFC4571: RTP & RTCP over Connection-Oriented Transport[12]
- • GB28181-2016: 公共安全视频监控联网系统信息传输、交换、控制技术要求[13]
- • ISO13818-1-2000[14]: MPEGPS(Program Stream), PS媒体流规范。
SIP Parser
C++没有特别好的SIP库,这也是SIP处理不稳定的一个原因。
不过发现SIP协议和HTTP协议结构非常一致,因此SRS采用http-parser[15]解析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的注册流程:
- 1. 在设备设置好SIP服务器为SRS。
- 2. 设备发送SIP格式的REGISTER消息。
- 3. SRS回应200/OK,注册成功。
GB的心跳:
- 1. 设备后面会不断发送MESSAGE作为心跳消息。
- 2. 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[16]。
至于媒体协议,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[17]。
- • 服务器重启后,可以使用缩短REGISTER的Expires,缩短心跳间隔,让设备重新注册,重新进入推流状态。服务器重启后,设备可以快速感知到媒体链路断开。
- • 传输过程中,若出现网络抖动导致链接断开,服务器和设备都可以很快感知到,进入异常处理流程。
因此,SRS先支持TCP,而不支持UDP。也就是先支持GB28181 2016,而不是支持GB28181 2011。
Note: 需要显式开启GB28181-2016,并开启TCP协议才可以。
Protocol Notes
SIP协议上特别需要注意的地方:
- • Via的branch必须是
z9hG4bK
开头,参考Via[18]的说明。 - • INVITE的200(OK)的ACK消息,ACK的Via的branch必须是新的,ACK并不是INVITE的transaction,参考Via[19]和Example[20]。
- • INVITE的Contact是自己的地址,而不是GB设备的,也就是Contact应该由From生成而不是To,参考Contact[21]和Example[22]。
- • INVITE的Subject,定义为
媒体流发送者ID:发送方媒体流序列号,媒体流接收者ID:接收方媒体流序列号
,参考附录K[23]。对于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[24]
- • 媒体正常传输过程中,信令重新注册,不影响媒体,继续正常传输。 参考 gb-media-ps-sip-register-loop.pcapng.zip[25]
- • 信令正常完成INVITE,媒体TCP端口若不打开,设备尝试连接一次后放弃。参考 gb-media-disabled-sip-ok.pcapng.zip[26]
- • 媒体正常传输过程中,信令断开,一定时间后,媒体断开。参考 gb-media-ps-sip-disconnect.pcapng.zip[27]
- • 媒体正常传输过程中,TCP连接断开,客户端不会重试。参考 gb-media-disconnect-sip-ok.pcapng.zip[28]
媒体协议:
- • 解析媒体流时,可能会出现各种错误,此时会丢弃整个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[29],就是前两个字节是长度,后面是RTP包。
Note: Wireshark支持RFC4571[30],它的Dissecotr是
rtp.rfc4571
。
有两种方法,一种直接打开包后,输入过滤tcp.port==9000
,然后右键包,选择Decode as > RTP
,就可以看到解析成了RFC4517,如下图所示:
;
Decode As RTP
还有一种方法,直接加载SRS的research/wireshark/gb28181.lua[31]插件,将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的媒体数据,如下图所示:
Wireshark GB28181 Plugin
Note: 注意RTP的Payload就是MPEG-PS[32],开头是
00 00 01 BA
的标识符,不过Wireshark不支持PS流解析。
工具准备好了,分析起来也会更方便。
Lazy Sweep
GB存在和Source清理[33]一样的问题。在GB中,存在SIP连接协程,媒体连接协程,会话协程等多个协程,这些协程之间会互相引用对象,而它们的生命周期是不一致的。
比如:SIP连接,需要持有会话对象的指针,当设备连接到SRS时,需要更新会话协程的SIP连接对象,这样会话需要发送信令消息,就可以走最新的SIP连接发送。
比如:媒体连接,收到媒体PS pack时,需要通知会话协程处理,转成RTMP流。媒体连接断开时,需要通知会话协程,会话协程会发送BYE和重新INVITE,通知设备重新推流。
比如:会话对象,有自己的生命周期,简单设计就是和Source一样永远不清理,这样它生命周期就会比SIP和媒体协程活得更长,这样它们引用会话对象时就是安全的,但这样就会有内存不释放的问题。同样,SIP连接一定需要清理,所以会话对象就可能会持有野指针问题。
Source清理的问题,本质上是多个协程之间生命周期不同步,所以如果释放Source后可能有些协程活得比Source更久,就可能出现野指针引用。详细请查看#413[34]的描述。
而这些问题的解决方案都是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[35],我们会基于Go的各种库实现GB的自动测试,也可以用作模拟摄像头。使用到的库包括:
- • ghettovoice/gosip[36]:SIP协议栈,这个库WebRTC段维伟也有贡献,模拟GB的SIP。
- • gomedia/mpeg2[37]:打包PS流,包括Pack头、System头、PSM包、音视频PES包等。
- • pion/rtp[38]:打包RTP头。由于GB的SDP不标准,所以没用pion的SDP解析,直接字符串查找SSRC即可。
- • pion/h264reader[39]: 读取h264格式的视频文件,在压测工具中,使用FFmpeg将FLV转成h264格式的视频文件,方便测试时分开测试音视频。
- • go-oryx-lib/aac[40]: 读取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最厉害的是这些控制机制,覆盖得非常全面,比如:
- 1. 启动三个协程,必须等三个协程结束后才能退出,并判断错误结果,决定测试是否成功。
- 2. 推流和播放协程,必须等待主协程初始化完毕才能启动。
- 3. 播放协程,必须等待推流建联后才能建联。
- 4. 所有协程,都不能超过测试用例的超时时间,比如5秒。
- 5. 若有协程异常,应该立刻结束,比如播放异常,推流就算正常也应该结束。
- 6. 若所有正常,也应该在一定包数之后结束,比如收发100个包,这样尽快可以跑完测试,比如100ms,而不是每个必须5秒(会导致整体测试时间太长)。
这些全都是控制机制,Go用了select+chan
、WaitGroup
、Context
三个基础组件就全部支持了,不得不佩服Go这个设计还是非常非常牛逼的。
Commits
和GB相关的修改:
- • HTTP: Support HTTP header in creating order. v5.0.68[41] GB的SIP头没有明确要求有序,不过倒是提过尽量优先,比如Via一般是放第一个。SIP使用HTTP协议栈解析,所以SIP的头有序,就改了HTTP支持头有序,按照添加的顺序,而不是默认的字母顺序。
- • Kernel: Support lazy sweeping simple GC. v5.0.69[42] 支持简单的延迟清理的GC,解决多个协程之间依赖对象的问题,未来Source清理[43]也可以使用这个机制。在GB上这个问题非常严重,因为有多个协程和多个对象互相依赖,清理时特别容易出现问题。此外,WebRTC over TCP也有两个协程的交互,也可以采用这个机制。
- • GB28181: Refine HTTP parser to support SIP. v5.0.70[44] 使用http parser解析SIP协议栈,本质上SIP和HTTP在协议格式上基本一致,在RFC中也说明了这一点。详细参考SIP Parser[45]中的说明。
- • RTC: Refine SDP to support GB28181 SSRC spec. v5.0.71[46] 使用RTC的SDP对象,解析和编码GB的SDP,主要是支持SSRC的格式
y=ssrc
的方式,以及一些不同的需要定义的字段。 - • ST: Support set context id while thread running. v5.0.72[47] 支持协程运行过程中改变ContextID,SIP和媒体线程运行后,从包中解析才知道对应的Session,然后将自己的ContextID设置为Session的,这样可以将日志全部打印到一个ContextID上,排查时可以查询这个ID即可。
- • HTTP: Skip body and left message by upgrade. v5.0.73[48] 解决HTTP Parser调用的问题,支持SIP这样可能多个Request或Response消息的情况,只解析头并且不解析剩下的数据,避免解析失败。
- • GB28181: Support GB28181-2016 protocol. v5.0.74[49] GB的主要逻辑,大约3.5K行左右,其他是测试代码大约4K行,以及srs-bench依赖的Go的第三方库。
Thanks
特别感谢夏立新等同学,两年前让SRS支持了GB功能,经过这两年的积累,我们形成了GB的开源社区,了解了GB的应用场景,以及主要的发展方向。
经过这两年对GB的理解,我们也终于有信心把GB合并到SRS 5.0,除了夏立新和陈海波,其中有非常多的同学的贡献,很抱歉无法一一表达。
在合并GB进SRS 5.0过程中,对于其中的难点和疑点,也有很多同学给与了帮助,包括王冰洋、陈海博、沈巍、周小军、夏立新、杜金房、姚文佳、潘林林等等同学。
引用链接
[1]
srs-gb28181: https://github.com/ossrs/srs-gb28181/issues/4 [2]
srs-gb28181: https://github.com/ossrs/srs-gb28181/issues/4 [3]
jsip: https://github.com/usnistgov/jsip [4]
srs-sip: https://github.com/ossrs/srs-sip [5]
http://localhost:8080/live/34020000001320000001.flv: http://localhost:8080/players/srs_player.html?stream=34020000001320000001.flv [6]
http://localhost:8080/live/34020000001320000001.m3u8: http://localhost:8080/players/srs_player.html?stream=34020000001320000001.m3u8 [7]
webrtc://localhost/live/34020000001320000001: http://localhost:8080/players/rtc_player.html?stream=34020000001320000001 [8]
WebRTC: Candidate: https://ossrs.io/lts/en-us/docs/v5/doc/webrtc#config-candidate [9]
Usage: #usage [10]
RFC3261: SIP: Session Initiation Protocol: https://www.ietf.org/rfc/rfc3261.html [11]
RFC4566: SDP: Session Description Protocol: https://www.ietf.org/rfc/rfc4566.html [12]
RFC4571: RTP & RTCP over Connection-Oriented Transport: https://www.ietf.org/rfc/rfc4571.html [13]
GB28181-2016: 公共安全视频监控联网系统信息传输、交换、控制技术要求: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=469659DC56B9B8187671FF08748CEC89 [14]
ISO13818-1-2000: https://ossrs.net/lts/zh-cn/assets/files/hls-mpeg-ts-iso13818-1-d21d1e9765012a327f03b43ce460079a.pdf [15]
http-parser: https://github.com/ossrs/http-parser [16]
RFC3261: Transport: https://www.ietf.org/rfc/rfc3261.html#section-18 [17]
Protocol Notes: #protocol-notes [18]
Via: https://www.ietf.org/rfc/rfc3261.html#section-8.1.1.7 [19]
Via: https://www.ietf.org/rfc/rfc3261.html#section-8.1.1.7 [20]
Example: https://www.ietf.org/rfc/rfc3261.html#section-24.2 [21]
Contact: https://www.ietf.org/rfc/rfc3261.html#section-8.1.1.8 [22]
Example: https://www.ietf.org/rfc/rfc3261.html#section-24.2 [23]
附录K: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=469659DC56B9B8187671FF08748CEC89 [24]
gb-media-ps-normal.pcapng.zip: https://github.com/ossrs/srs/files/9630224/gb-media-ps-normal.pcapng.zip [25]
gb-media-ps-sip-register-loop.pcapng.zip: https://github.com/ossrs/srs/files/9630227/gb-media-ps-sip-register-loop.pcapng.zip [26]
gb-media-disabled-sip-ok.pcapng.zip: https://github.com/ossrs/srs/files/9630216/gb-media-disabled-sip-ok.pcapng.zip [27]
gb-media-ps-sip-disconnect.pcapng.zip: https://github.com/ossrs/srs/files/9630218/gb-media-ps-sip-disconnect.pcapng.zip [28]
gb-media-disconnect-sip-ok.pcapng.zip: https://github.com/ossrs/srs/files/9630220/gb-media-disconnect-sip-ok.pcapng.zip [29]
RFC4571: RTP & RTCP over Connection-Oriented Transport: https://www.ietf.org/rfc/rfc4571.html [30]
RFC4571: https://github.com/wireshark/wireshark/commit/7eee48ad5588bc2debec0e564b3526c97a0eb125#diff-ef0e5a499517cb594820f7dfd9200ee5c3cf5bd32259e066464a40aa6eebfb1cR3601 [31]
research/wireshark/gb28181.lua: https://github.com/ossrs/srs/blob/develop/trunk/research/wireshark/gb28181.lua [32]
MPEG-PS: https://en.wikipedia.org/wiki/MPEG_program_stream [33]
Source清理: https://github.com/ossrs/srs/issues/413#issuecomment-1227972901 [34]
#413: https://github.com/ossrs/srs/issues/413 [35]
srs-bench: https://github.com/ossrs/srs-bench/tree/feature/rtc [36]
ghettovoice/gosip: https://github.com/ghettovoice/gosip [37]
gomedia/mpeg2: https://github.com/yapingcat/gomedia/mpeg2 [38]
pion/rtp: https://github.com/pion/rtp [39]
pion/h264reader: https://github.com/pion/webrtc [40]
go-oryx-lib/aac: https://github.com/ossrs/go-oryx-lib [41]
HTTP: Support HTTP header in creating order. v5.0.68: https://github.com/ossrs/srs/commit/4b7d9587f [42]
Kernel: Support lazy sweeping simple GC. v5.0.69: https://github.com/ossrs/srs/commit/927dd473e [43]
Source清理: https://github.com/ossrs/srs/issues/413 [44]
GB28181: Refine HTTP parser to support SIP. v5.0.70: https://github.com/ossrs/srs/commit/1e6143e2e [45]
SIP Parser: #sip-parser [46]
RTC: Refine SDP to support GB28181 SSRC spec. v5.0.71: https://github.com/ossrs/srs/commit/4ad4dd097 [47]
ST: Support set context id while thread running. v5.0.72: https://github.com/ossrs/srs/commit/dc20d5ddb [48]
HTTP: Skip body and left message by upgrade. v5.0.73: https://github.com/ossrs/srs/commit/cfbbe3044 [49]
GB28181: Support GB28181-2016 protocol. v5.0.74: https://github.com/ossrs/srs/pull/3201