支持GB28181是正确的事情,可能也是困难的事情,因为困难所以有趣。
在非常多朋友的努力下,SRS的GB功能不少,详细可以参考srs-gb28181[1]。由于GB和摄像头的复杂性,问题也是不少的,特别是稳定性问题,这也是为什么GB一直迟迟没有进SRS 5.0分支的原因。
现在SRS 5.0已经临近功能封版了,我们增加了几个大的功能和改进,最后一个功能就是在考虑是否支持GB。鉴于GB目前的稳定性表现,肯定不能完全合并过来,是否能有稳定性更高的合并办法?
如果减少功能,当然稳定性就会提升,所以SRS 5.0可能的合并方式,就是只合并一个最简单的GB的能力,我们就叫它PoC吧。我列一下我知道的GB的功能清单:
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能做多好。
首先,编译和启动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会自动邀请摄像头推流,可以打开下面的链接播放:
Note: 请把流名称换成你的设备名称,然后点播放。
由于我家里设备不方便长时间推流测试,我最多推流了13小时还是正常的,欢迎大家用专门的测试环境和设备测一测,我们第一个小目标是不间断推流一个月。
GB的Candidate定义和WebRTC: Candidate[8]概念上一致,都是需要暴露一个客户端能访问的IP地址,在SDP中传递给客户端。比如:
stream_caster.sip.candidate
,SRS启动会读取这个配置,比如192.168.1.100
。IN IP4 192.168.1.100
。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自己发现。
GB相关的协议如下:
C++没有特别好的SIP库,这也是SIP处理不稳定的一个原因。
不过发现SIP协议和HTTP协议结构非常一致,因此SRS采用http-parser[15]解析SIP,这个库是nodejs维护的,之前好像是NGINX中扒出来的,所以稳定性还是非常高的。
当然用HTTP解析SIP,需要有些修改,主要是以下修改:
REGISTER
、INVITE
、ACK
、MESSAGE
和BYE
,这是GB常用的几个消息。sip:xxx
格式,会被认为是HTTP完整URL格式导致解析失败。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能正确处理这种情况。
GB的注册流程:
GB的心跳:
SRS若重启后,由于没有保存任何状态,所以收到的可能是设备的MESSAGE消息,而没有REGISTER消息,所以希望设备能重新注册。向各位同学以及SIP和GB的专家请教后,重新注册的可能方法包括:
A.2.3 控制命令
,远程启动是<TeleBoot>Boot</TeleBoot>
,尝试重启设备后会重新注册。这个还没验证。kill -9
或者系统OOM,不会给程序机会清理,所以这个不能适应所有场景。不过在主动升级时,一般会用Gracefully Quit,这时可以有机会处理这个问题,大家可以尝试。总之,是没有特别可靠的办法能让摄像头立刻重新注册,SRS必须在逻辑上处理这个问题:SRS启动或重启后,摄像头还在已经注册,甚至在传输流的状态。
Note: 由于很多问题都是持续长时间运行,而系统的某一方重启了,导致状态不一致,引起各种问题。因此,在SRS重启或者启动时,若发现有摄像头是在注册或传输流的状态,那么应该尝试让摄像头重新走一次流程,比如重新注册和重新推流,这样让双方的状态一致,可靠性会更高。
Note: 验证发现,重新注册,对正在传输的媒体流不影响。设备会探测端口可达性,如果TCP断开,或者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的好处:
因此,SRS先支持TCP,而不支持UDP。也就是先支持GB28181 2016,而不是支持GB28181 2011。
Note: 需要显式开启GB28181-2016,并开启TCP协议才可以。
SIP协议上特别需要注意的地方:
z9hG4bK
开头,参考Via[18]的说明。媒体流发送者ID:发送方媒体流序列号,媒体流接收者ID:接收方媒体流序列号
,参考附录K[23]。对于s=Play
实时观看的场景,接收方媒体流序列号(SSRC)其实没有定义;根据各位同学反馈,一般这个字段填0。SDP协议上特别注意的地方:
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。
信令和媒体配合:
媒体协议:
00 00 01 ba
)。其中包括RTP解析失败,非法的PS头(非00 00 01
开头),部分PES头(比如在前一个TCP包的尾部),甚至还有RFC4571的包解析失败(头两个字节代表的长度信息是0)。媒体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默认就能解析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流解析。
工具准备好了,分析起来也会更方便。
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对象,没有任何其他特别的地方,看起来只是加了一层函数而已,是最简单的方案。
GB缺乏工具链,基本上是空白的,而没有工具,就只能借助真实的摄像头测试,这基本上就是原始时代:
和WebRTC一样,SRS也会完善GB的工具链,参考srs-bench[35],我们会基于Go的各种库实现GB的自动测试,也可以用作模拟摄像头。使用到的库包括:
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最厉害的是这些控制机制,覆盖得非常全面,比如:
这些全都是控制机制,Go用了select+chan
、WaitGroup
、Context
三个基础组件就全部支持了,不得不佩服Go这个设计还是非常非常牛逼的。
和GB相关的修改:
y=ssrc
的方式,以及一些不同的需要定义的字段。特别感谢夏立新等同学,两年前让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