Skip to main content
版本:5.0 (Stable) ✅

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会自动邀请摄像头推流,可以打开下面的链接播放:

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中传递给客户端。比如:

  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中配置在客户端的。

Note: 由于GB的SIP协议,在REGISTER时To字段并没有带服务器的地址,所以导致服务器无法从SIP中发现自己的地址,只能依靠服务器配置。

当然,如果网卡配置了客户端可以访问的地址,可以把CANDIDATE配置为*,让SRS自己发现。

Latency

与普遍认知相反,安全摄像头并非天生就是低延迟系统。它们的主要目的是长期存储。在平移-倾斜-缩放(PTZ)场景中,可能存在一定的延迟要求, 但这并不是我们通常所说的低延迟。PTZ摄像头的延迟通常在1秒左右,这被认为是可以接受的,而实时通信(RTC)中的低延迟通常在200ms左右。

延迟不仅与服务器有关,还涉及到整个链路。关于详细的延迟优化,请参考Low Latency。 我使用默认配置测试了海康威视摄像头的延迟,如下所示:

  • 流类型:子流
  • 视频类型:复合流
  • 分辨率:640x480
  • 码率类型:可变码率
  • 图像质量:中等
  • 视频帧率:25fps
  • 码率限制:1024Kbps
  • 视频编码:H.264
  • 编码复杂度:中等
  • I帧间隔:50
  • SVC:禁用
  • 平台接入协议:GB/T28181-2016
  • 平台传输协议:TCP,即通过GB/TCP将流推送到SRS
  • 观看:WebRTC(UDP)
  • SRS版本:v5.0.100
  • 摄像头和服务器:内网
  • 延迟:358ms

Features

目前SRS支持的GB的功能清单:

  1. 摄像头通过SIP注册。srs-gb28181支持。SRS 5.0 支持
  2. 自动邀请摄像头推流。srs-gb28181支持。SRS 5.0 支持
  3. GB/2016转RTMP协议。srs-gb28181支持。SRS 5.0 支持
  4. 基于TCP的SIP信令。srs-gb28181支持。SRS 5.0 支持
  5. TCP单端口传输媒体。srs-gb28181支持。SRS 5.0 支持

目前还没有支持的GB功能:

  1. 基于UDP的SIP信令。srs-gb28181支持。SRS 5.0 不支持。
  2. UDP单端口传输媒体。srs-gb28181支持。SRS 5.0 不支持。
  3. GB/2011转RTMP协议。srs-gb28181支持。SRS 5.0 不支持。
  4. UDP/TCP多端口传输媒体。srs-gb28181支持。SRS 5.0 不支持。
  5. HTTP API查询GB流。srs-gb28181支持。SRS 5.0 不支持。
  6. HTTP API云台摄像头。srs-gb28181支持。SRS 5.0 不支持。
  7. Web管理页面。srs-gb28181支持。SRS 5.0 不支持。
  8. GB下级服务器。srs-gb28181不支持。SRS 5.0 不支持。
  9. GB语音对讲。srs-gb28181不支持。SRS 5.0 不支持。
  10. GB回看。srs-gb28181不支持。SRS 5.0 不支持。
  11. GB加密传输。srs-gb28181不支持。SRS 5.0 不支持。

希望大家降低期望,GB的坑太难填了,希望不要期待SRS能做多好。

Protocols

GB相关的协议如下:

SIP Parser

C++没有特别好的SIP库,这也是SIP处理不稳定的一个原因。

不过发现SIP协议和HTTP协议结构非常一致,因此SRS采用http-parser解析SIP,这个库是nodejs维护的,之前好像是NGINX中扒出来的,所以稳定性还是非常高的。

当然用HTTP解析SIP,需要有些修改,主要是以下修改:

  • Method:需要新增几个方法,比如REGISTERINVITEACKMESSAGEBYE,这是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

至于媒体协议,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,参考ViaExample
  • INVITE的Contact是自己的地址,而不是GB设备的,也就是Contact应该由From生成而不是To,参考ContactExample
  • 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。

信令和媒体配合:

媒体协议:

  • 解析媒体流时,可能会出现各种错误,此时会丢弃整个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.h264avatar.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+chanWaitGroupContext三个基础组件就全部支持了,不得不佩服Go这个设计还是非常非常牛逼的。

Commits

和GB相关的修改:

Thanks

特别感谢夏立新等同学,两年前让SRS支持了GB功能,经过这两年的积累,我们形成了GB的开源社区,了解了GB的应用场景,以及主要的发展方向。

经过这两年对GB的理解,我们也终于有信心把GB合并到SRS 5.0,除了夏立新和陈海博,其中有非常多的同学的贡献,很抱歉无法一一表达。

在合并GB进SRS 5.0过程中,对于其中的难点和疑点,也有很多同学给与了帮助,包括王冰洋、陈海博、沈巍、周小军、夏立新、杜金房、姚文佳、潘林林等等同学。