SRS总结 - 1
RTMP是Adobe 公司为 Flash 播放器和服务器之间音视频数据传输开发的私有协议,因为出现的比较早,所以RTMP协议已经成为国内直播领域尤其是CDN之间推流的标准协议。
Adobe在2017年宣布到2020年底将不再支持Flash,所以很多系统平台的浏览器也都不再支持RTMP协议,如果流媒体服务器只支持RTMP协议,则最新的浏览器就无法通过无插件的方式从服务器获取媒体流,所以SRS服务器有个很重要的工作就是针对音视频数据的转码。
例如,HLS(HTTP Live Streaming)是一个被各种流媒体播放客户端广泛支持的标准协议,SRS流媒体服务器将RTMP推流端发送的音视频数据,转换为满足HLS协议要求的m3u8文件和ts文件,最终,浏览器通过HTTP协议从服务器获取m3u8文件和ts文件并实现本地播放。
1.分析代码的准备
1.1.下载及编译
git clone -b develop https://gitee.com/ossrs/srs.git && cd srs/trunk && ./configure && make && ./objs/srs -c conf/srs.conf
1.中间如有提示需要安装什么软件,根据提示进行安装,接着后面的步骤进行。
2.首次分开执行,便于找错误。
1.2.如何用GDB调试 SRS
1.修改配置文件中daemon 为 off。
2.正常用GDB调试即可。如:
cd /opt/srs/trunk
gdb ./objs/srs -c conf/srs.conf
gdb ./objs/srs -c conf/rtmp2rtc.conf
3.先设置断点,再运行,不然出错。
1.3.检验是否成功
1.Open http://localhost:8080/ to check it(http://10.10.14.103:8080/ )
2. ./etc/init.d/srs status
3. ps -ef | grep srs
1.3.推送视频流
e:
cd e:\Demo\CGAvioRead\Debug
ffmpeg -re -stream_loop -1 -i d:/H264_AAC_2021-02-10_1080P.mp4 -vcodec copy -acodec copy -f flv -y rtmp://10.10.15.30:1935/live/livestream
1.4.视频的播放
1.VLC
2.SRS播放器
RTMP (by VLC): rtmp://192.168.0.109:1935/live/livestream
H5(HTTP-FLV): http://10.10.14.103:8080/live/Camera_00001.flv
H5(HLS): http:// 10.10.14.103:8080/live/Camera_00001.m3u8
3.ffplay 播放
ffplay -fflags nobuffer -flags low_delay -i rtmp://192.168.0.109:1935/live/livestream
ffplay http:// 192.168.0.109:8080/live/livestream.flv
总结:1.编译时如果提示什么错误,按错误进行修改就行。
2.rtmp推送时一般用obs,用ffmpeg时不知是参数设置的问题,还是其他问题,用wireshark截图,不好分析。
3.看srs是否运行成功可用自身(网页、所带程序)、ps、lsof等检测。
4.播放时可用VLC、srs播放器、ffplay播放,一般用vlc.
1.5wireshark截图
1.5.1.推流
下H264视频帧的FLV封装格式:
下AAC音频帧的FLV封装格式:
1.5.2.拉流
1.5.3 sps、pps
1.5.4.video
1.5.5.audio
1.5.6.客户端与服务器之间的协议交换过程
标准查询:
https://www.rfc-editor.org/search/rfc_search_detail.php
2.SRS推流过程
2. 1.main🡪 do_main --srs_main_server.cpp
参考:https://www.xianwaizhiyin.net/?p=1391
调试:
gdb ./objs/srs -c conf/srs.conf(通过调试可知,conf/srs.conf为默认参数,可以不加,直接写为./objs/srs)。
涉及的主要文件:srs_main_server.cpp、srs_app_threads.cpp、srs_app_config.cpp
SRS 源码里 其实有 3 个 main() 函数,分别在srs_main_ingest_hls.cpp、srs_main_mp4_parser.cpp、srs_main_server.cpp 3个文件里面。不过srs可执行文件,是srs_main_server.cpp生成的,所以先分析srs_main_server.cpp,其他两个文件不管。
总结:1.srs运行时,如果不指定配置文件名,默认的为conf/srs.conf。
2.srs程序开始的函数为srs_main_server.cpp中的main函数。
3.在调试时要将srs.conf中daemon 改为off,使srs在前台运行而不任为后台运行。
4. SRS 的所有业务都是基于ST协程实现的,没有用线程。
5. srs只支持小端序机器,大端序机器不支持。网络是大端序机器。
2.1.1.定义了一些全局变量:
_srs_config:全局配置文件,_srs_log:全局的log文件
日志文件为:/objs/srs.log。
2.1.1.main() 函数的流程图
main()函数的内部逻辑实际上比较简单,因为所有的操作都封装在其他函数里面。特别是srs_thread_initialize()跟run_directly_or_daemon()函数。
- srs_thread_initialize:Initialize global or thread-local variables.
- srs_thread_initialize()中可看到_srs_log = new SrsFileLog();
2.1.2函数解释
1.srs_thread_initialize()里面有非常多的初始化操作,日志操作,配置文件,等等。
2.srs_assert(srs_is_little_endian());srs只支持小端序机器,大端序机器不支持。
3.用了大量的GPERF来检测内存泄漏。默认状态没有启用。
4.show_macro_features(),这个函数打印srs支持哪些功能例如 srt、dvr是否支持。如在日志文件里:features, rch:on, dash:on, hls:on, hds:off, srt:off, hc:on, ha:on, hs:on, hp:on, dvr:on, trans:on, inge:on, stat:on, sc:on。
5.run_directly_or_daemon(),此函数开始运行srs,可能在前台运行,也可能以守护进程运行。
创建SrsServer时还会初始化http_api_mux和http_server:
http_api_mux = new SrsHttpServeMux(); // HTTP请求多路复用器,不是http拉流的
http_server = new SrsHttpServer(this); // http服务
- main—》do_main—》run_directly_or_daemon—》run_hybrid_server—》SrsServerAdapter::SrsServerAdapter()—》srs = new SrsServer();
- 默认注册了两个服务:
_srs_hybrid->register_server(new SrsServerAdapter());
_srs_hybrid->register_server(new RtcServerAdapter());
6.listen的调用过程
(1)SrsHybridServer::run—》SrsServerAdapter::run—》srs->listen()
(2)SrsServer::listen创建各种监听。
(3)SrsServer::listen()—>SrsServer::listen_rtmp🡪 SrsBufferListener::listen🡪SrsTcpListener::listen()🡪SrsSTCoroutine::start()🡪SrsFastCoroutine::start🡪SrsFastCoroutine::pfn🡪SrsFastCoroutine::cycle。
7. SrsTcpListener类进行实际的监听,通过socket->bind->listen(在srs_tcp_listen函数中完成)创建监听的fd,并将fd注册到st库上,之后fd上的事件都有st库监听并处理。
8. 创建tcp协程,用于处理连接,协程启动,并进入 SrsSTCoroutine::cycle() 函数。最后会调用到 SrsTcpListener::cycle()。cycle()函数用于处理客户端连接。监听协程接受连接请求后将执行逻辑交BufferListener处理, handler->on_tcp_client(fd)。--》SrsBufferListener::on_tcp_client()--server->accept_clien🡪 SrsServer::accept_client-- conn->start()
(1) fd_to_resource(type, stfd, &conn)) != srs_success🡪*pr = new SrsRtmpConn(this, stfd, ip, port);
(2)SrsRtmpConn::start—》SrsRtmpConn::cycle
2.1.3大端与小端
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,数据从高位往低位放;这和我们的阅读数字习惯一致。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
网络传输为大端模式,计算机是小端模式。
2.2. RTMP入口
参考:https://blog.csdn.net/u012117034/article/details/124107654
调试:gdb ./objs/srs
1. run_directly_or_daemon()- srs_main_server.cpp
(1)用了比较多的srs_trace() 函数来记录日志。srs_trace() 函数会往 srs.log 写入一条日志。
(2) SRS的守护进程没用setsid() 跟 umask(022) ,也就是当前进程没有脱离从父进程继承的SessionID、进程组ID和打开的终端。
(3)调用 run_hybrid_server()。
2.run_hybrid_server() - srs_main_server.cpp
调试:gdb ./objs/srs
没有执行SRS_SRT,其他的执行了
(1)利用依赖注入把Srs、Srt、Rtc的 Adapter注入给 _srs_hybrid。里面其实是一个vector,std::vector<ISrsHybridServer*> servers
(2)然后初始化 _srs_hybrid,SRS 是一个混合的服务器,他结合了 RTMP、SRT、webrtc,所以叫 hybird。
(3)_srs_circuit_breaker 具体的作用后面补充,可能是类似一个 watchdog 的机制(TODO)。
(4)_srs_hybrid->run() 应该就会开启协程,然后一直阻塞在这里。
3._srs_hybrid->run()-- srs_app_hybrid.cpp
SrsHybridServer::run()🡪server->run(&wg)
_srs_hybrid->run() 的代码比较简单,就是遍历之前注入的 vector,然后执行他们的 run 函数。RTMP 应该是在 SrsServerAdapter 里面处理的,而不是 Srt 或者 RTC 的 Adapter。所以只需要找 SrsServerAdapter 的 run 函数就行。为了便于理解,先画个结构图,如下:
4. ISrsHybridServer 类
上图的重点是 =0 这种语法是纯虚函数的写法,意思是把这个函数指针赋值为 0。
- 定义一个函数为虚函数,不代表函数为不被实现的函数。
- 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
- 定义一个函数为纯虚函数,才代表函数没有被实现。
- 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
Srs、Srt、Rtc 都会继承这个 ISrsHybridServer 类,实现 initialize (初始化),run (运行),stop(停止)函数。
5.SrsServerAdapter::run(SrsWaitGroup* wg)
从上面代码可以看出,run 里面调了相当多的 class SrsServer 里面的函数。如下:
(1)srs->initialize(),ch为NULL,这个函数里面初始了几个http服务器,但是还没开始 listen.
(2)srs->initialize_st(),这个函数跟st库没有关系,主要是对supervisor(主管人)的场景做处理。没看懂。
(3)srs->acquire_pid_file(),生成 pid 进程文件。
(4)srs->initialize_signal(),对信号做处理,应该会把信号转成IO事情。
(5)srs->listen(),开始监听端口了,listen fd 会保存在对象里面,一个协程监听一个listen fd。监听各种协议。
6)srs->register_signal(),还是跟信号有关。-- signal_manager->start()
7)srs->http_handle() 处理HTTP 请求。-- http_api_mux->handle
8)srs->ingest():start thread to run all encoding engines。
9)srs->start(wg),启动,这个wg是重点后面会分析。
上面一共调了 9 个函数,但是实际上只有 两个 重点函数,srs->listen() 跟 srs->start(wg)。
srs->http_handle() 虽然也是重点先不做展开。
SRS 的http 服务器好像是自己写的, http 报文的解析在trunk\src\protocol\srs_http_stack.hpp 文件。
6.SrsServer::listen()
srs->listen(),这个函数是重中之中,开始监听端口了,代码如下:
SRS 启动之后,只看到一个进程,而且搜索源代码,也没发现 pthread_create() 的函数在 SRS的代码里面没出现,也就是说 SRS 的所有业务都是基于ST协程实现的,没有用线程。
7.SrsServer::listen_rtmp()--srs_app_server.cpp
调试:gdb ./objs/srs -c conf/srs.conf
(1)ip:0.0.0.0 port:1935
(2)listener->listen
把往Srsserver类的 private变量std::vector<SrsListener*> listeners 插数据,因为 RTMP 可以监听多个IP跟端口。然后调 SrsBufferListener::listen(),然后再调 SrsTcpListener::listen(),这个链路有点长,如下:
所以重点 在TCP 的listen 函数里面,RTMP 是基于 TCP 的,所以肯定是会listen 一个tcp的fd,现在就深入看 SrsTcpListener::listen()。
8.SrsTcpListener::listen()--srs_app_listener.cpp
(1)如上图,SrsTcpListener 类里面有个变量 srs_netfd_t lfd,l是 listen的缩写。这个 srs_netfd_t 实际上就是 st 库里面的 st_netfd_t,只是换了个命名。SRS 代码的数据结构,有很多都是用 st 的数据结构,例如条件变量、互斥锁等等。
(2)ip:0.0.0.0 port:1935
if ((err = srs_tcp_listen(ip, port, &lfd)) != srs_success)
这个函数的调用链还是有点长,我还是画个流程图吧。
上图最重要的其实是 srs_netfd_open_socket() 这个其实是 st 库的函数。
9.do_srs_tcp_listen
srs_tcp_listen() 函数执行完之后,就已经拿到了 ST 库的 netfd,就会开始创建协程。SrsSTCoroutine 继承 SrsFastCoroutine,所以这里创建协程使用的是 SrsFastCoroutine::start() 函数,SrsFastCoroutine 类里面有个 srs_thread_t ,这实际跟 ST 库的 _st_thread_t 是一样的。
10.SrsFastCoroutine::start()
(1)if ((trd = (srs_thread_t)_pfn_st_thread_create(pfn, this, 1, stack_size)) == NULL)
start() 函数里面调的就是ST库的创建协程的函数。所以 srs_tcp_listen() 函数执行完之后,就已经拿到了 ST 库的 netfd,就调 SrsFastCoroutine::start() 创建一个协程。注意这里 _pfn_st_thread_create() 传递的 是 pfn, 所以协程的 start 函数 是 SrsFastCoroutine::pfn(),上下文切换的时候,这个协程会从 SrsFastCoroutine::pfn() 函数开始执行。协程 start 函数 的参数是 this,就是对象自己至此,虽然还有一点东西没讲,但是整体的流程图已经可以画出来了,如下:
从上图可以看出,listen_rtmp()、listen_http_api()等等函数都会创建一协程SrsServer::listen() 执行完之后,一共创建了 7 个协程。但是这 7个协程还未开始运行。因为还没开始切换上下文。
11.SrsServer::start()
(1)SrsServer::start() 函数实际上是把自己也变成一个协程,丢进去 RUNQ 里面了,this 是 SrsServer 对象。
此时此刻,协程还是没开始运行。
上面流程图中的 SrtServerAdapter::run() 是创建 SRT 相关的协程, RtcServerAdapter::run() 是创建 RTC 相关的协程,这些不是本文重点,不用管。
(2)此时此刻,协程还是没开始运行。那什么时候协程开始运行?在上面截图中,SrsServer::start() 函数最后有两句代码:
每个 Server start的时候都会往 wg add 一下。
上面的流程图,用绿色画出了一个框,wg.wait(),真正开始切换上下文,让之前创建的协程全部跑起来,猜测就是在这里做的。
(3)SrsWaitGroup::wait() 的实现非常简单,就是等待一个协程条件变量。
void SrsWaitGroup::wait()
{
if (nn_ > 0) {
srs_cond_wait(done_);
}
}
注意,这里的 srs_cond_wait() 会让当前协程阻塞,实际上是切换到其他地方开始执行,这是 ST 的函数,ST 的阻塞函数就会导致上下文切换,进入 _st_vp_schedule(),开始把之前创建的协程拿出来,一一运行起来。代码运行到 wg.wait() 的时候,之前的协程已经开始跑起来,那 RTMP 会在哪个地方跑起来呢?之前说过,协程 start 函数 是 SrsFastCoroutine::pfn(),所以 RTMP 的业务会在这个函数 pfn() 函数跑起来,实际上SRT 、RTC也是在这个 pfn() 函数跑起来的。
12. SrsFastCoroutine::pfn(void* arg)
srs_error_t err = p->cycle();--》SrsFastCoroutine::cycle()—》handler->cycle();
可以看到,pfn() 实际上是调了子类的 cycle() 来循环处理业务。那 RTMP 业务的子类是啥?是 SrsTcpListener ,所以需要看 SrsTcpListener 的 cycle 实现。
13.SrsTcpListener::cycle()
(1)srs_netfd_t fd = srs_accept(lfd, NULL, NULL, SRS_UTIME_NO_TIMEOUT);
直接用 ST 库的函数 srs_accept() 阻塞,等待 tcp 客户端来。然后丢给 handler 的 on_tcp_client 处理逻辑。
(2)handler->on_tcp_client()
当初初始化 RTMP 的时候,handler 传的是什么?请看下图:
从上图可以看到 传的是 this,肯定又是子类传参法。所以 handler->on_tcp_client() 的实现如下:
(3)现在又有一个疑问,上面的 server 是什么?请看下图:
从上图可以看到,传的是this,所以 server 就是 SrsServer,所以 RTMP 接受到一个 tcp 客户端 fd 的时候,就会执行 SrsServer::accept_client() 函数下面开始分析 SrsServer::accept_client() 函数,代码如下:
- 先根据type获取连接的SrsConnection
- 将SrsConnection加入SrsResourceManager::add🡪conns_,conns_存放所有的连接.
- 为每一个SrsConnection开启一个连接协程
上图的重点是 fd_to_resource() 函数,代码如下:
调试:
gdb ./objs/srs -c conf/srs.conf
设置断点时不要加括号。
ip:推流端的IP。
port:推流端的端口号。
此时此刻,已经追踪到了 rtmp 连接的处理入口,就是 new SrsRtmpConn()。从上图能看到,RTMP,HTTP 是同一个地方处理的。所以 SrsServer 实际上就是处理 RTMP、http等请求的。从 pfn() 到 cycle() 到 srs_accept() 再到 rtmp 的入口,这个链条有点长,画个流程图便于理解。
到这里,已经找到 RTMP 业务的入口了,就是 new SrsRtmpConn()。
- 因为现在type是SrsListenerRtmpStream,所有conn返回的是SrsRtmpConn。
- SrsConnection::start()🡪trd->start--启动conn协程,最后会执行到SrsConnection::cycle()
2.3.创建RTMP协程
参考:https://www.xianwaizhiyin.net/?p=1428
gdb ./objs/srs -c conf/srs.conf
SrsRtmpConn(SrsServer* svr, srs_netfd_t c, std::string cip, int port)
cip:客户端IP,cport:客户端端口。
注意上面第二个参数srs_netfd_t c ,这个是 ST 库的 fd,对原始的 tcp fd 封装了一下。SrsRtmpConn::SrsRtmpConn() 只有一个重点,就是创建了一个协程来处理这个 客户端的 TCP 链接。
1._srs_context->set_id(_srs_context->generate_id());
srs_context 是一个全局变量,主要是用来生成唯一ID,做日志跟踪的。这个变量跟上下文切换没有太大关系。SRS 里面 以 _ 开始的变量全都是全局变量。
2.skt = new SrsTcpConnection(c);
上面是创建一个Tcp链接管理器,方便后面对这个 fd 进行读写。
3.clk = new SrsWallClock();
类 SrsWallClock 应该是一个时钟。
4.kbps = new SrsKbps(clk);
类 SrsKbps 是用来统计 IO 流量的。
5.new SrsSTCoroutine()
这个函数应该就是 SRS 创建协程的封装函数。
原型:SrsSTCoroutine(std::string n, ISrsCoroutineHandler* h, SrsContextId cid);
调用:trd = new SrsSTCoroutine("rtmp", this, _srs_context->get_id());
第一个参数 是 rtmp,这个是个字符串,标记是什么类型的协程。
第二个参数 h就是重点,全部都是用 this,这是一种多态用法。这个 this 类要实现一个 cycle() 函数,然后协程运行的时候,就会跑到 cycle() 函数不断循环处理,在此刻 this 是 SrsRtmpConn。
第三个参数 cid 是协程ID。可以理解为生成一个唯一标示。
所以,如果你要二次开发,你创建自己的协程的时候,需要新建一个类,实现一个 cycle() 函数,然后 丢进去 new SrsSTCoroutine() 函数就行。
这里虽然创建了协程,但是当前逻辑还没执行到 ST 库的阻塞函数,所以新创建的协程还未开始运行,代码还是会继续往下走。注意一点,用ST的协程,只有遇到阻塞函数才会开始切换上下文。
6.rtmp = new SrsRtmpServer(skt);
这个 SrsRtmpServer 类,不是监听端口,而是处理 RTMP握手逻辑的,可以理解为RTMP链接的管理器。这里提及一下,RTMP协议的实现大部分都在 srs_rtmp_stack.cpp 文件里面。
7. refer = new SrsRefer();
检测 refer,跟 http 的 refer 差不多,来源。
8. bandwidth = new SrsBandwidth();
带宽检测CDN,由于SRS刚开始的业务场景是 CDN,所以需要计算流量做带宽限制之类的。
9. security = new SrsSecurity();
这个是安全检测,允许哪些客户端可以进行推流拉流。
10. wakable = NULL;
这个 wakable 变量比较有趣,注意一下。
11. mw_sleep = SRS_PERF_MW_SLEEP;
这个是协程休眠阻塞,等收到了8个音视频包后,才会转发给播放器,达到合并写功能,能优化IO效率。
12. _srs_config->subscribe(this);
上面这句代码应该是为了 reload 配置文件的时候,能有所响应,应该是注册一个 reload的处理事件。
执行完上面的代码之后,SrsRtmpConn::SrsRtmpConn() 这个函数就退出了,只需要记得,里面创建了一个协程 函数 SrsRtmpConn::cycle()。
到这里,SrsRtmpConn::SrsRtmpConn() 构造函数已经分析完毕。
具体在 TCP 的基础上建立 RTMP 链接的逻辑就会在 协程 SrsRtmpConn::cycle() 里面处理。
2.4. SrsRtmpConn::cycle
参考:https://www.xianwaizhiyin.net/?p=1438
如果有推流事件,就会进入SrsRtmpConn::do_cycle(),此函数负责具体执行RTMP流程,包括握手,接收connect请求,发送response connect响应,以及接收音视频流数据等处理。
1.RTMP 的握手逻辑全部在 rtmp->handshake() 里面,SRS 的 RTMP 服务器实现,是先尝试复杂握手,不行再切换成简单握手。(具体分析见:3.结合代码分析握手协议)
2. Class SrsRequest
上面 do_cycle() 里面用到了一个 Class SrsRequest ,这个指的是 RTMP request,因为 SRS 早期的全称 是 Simple RTMP Server。虽然现在 SRS 服务器混合了 SRT 和 Webrtc,但是这个 SrsRequest 跟Srt 跟 RTC 没有关系,大部分的类、方法、变量名,前面如果是 Srs,都可以把它看成是 RTMP 相关的业务。
3. 分析下面两句代码
SrsRequest* req = info->req;
rtmp->connect_app(req)
代码跑到这里的时候,客户端已经开始发 connect 指令,connect_app() 函数做的事情就是 把客户端的 connect 请求的信息提取出来,放到 req 变量。
4. SrsRtmpConn::service_cycle()
SrsRtmpConn::service_cycle() 函数前面的一堆逻辑,都是处理 RTMP 协议的交互协商的,就是 window size,chunk size,bandwidth之类的。里面最重要的地方是调了 stream_service_cycle()。
5.实际上到这里,RTMP 的协议的握手、chunk size、窗口、带宽,都已经交互完毕,最后就是循环执行 SrsRtmpConn::stream_service_cycle() 。stream_service_cycle() 函数,看名字就知道是处理流的,没错,这个函数是处理 RTMP 推流,跟播放两个业务的。
2.5. SrsRtmpConn::stream_service_cycle
参考:https://www.xianwaizhiyin.net/?p=1444
1.这里插个题, 在调 stream_service_cycle() 之前,调了 trd->pull(), trd->pull() 在很多地方都出现,应该是个重点函数,具体另起一篇文章分析。本文暂时跳过。stream_service_cycle() 开头有一些 RTMP edge集群的逻辑,这块先跳过不管,本文环境没配置集群,不会跑进去那块逻辑。
2.因为 info->type 等于 SrsRtmpConnFMLEPublish,所以执行的rtmp->start_fmle_publish() ,这个函数是做推流交互处理的,fmle 是什么缩写我也不太清楚,埋个坑,后面填。下图 wireshark 圈出来的交互部分就是这个函数做的。
start_fmle_publish() 函数里面使用了 expect_message() ,expect_message() 函数会阻塞等待客户端的RTMP包来,然后按顺序处理,完成整个推流的前期交互逻辑,例如流名称是啥,客户端总得先告诉服务器再推流。
3._srs_sources->fetch_or_create() 创建了一个 SrsLiveSource。用 SrsLiveSource 来管理推流。_srs_sources 全局变量是在 srs_thread_initialize() 函数里面初始化的,代码如下:
_srs_sources = new SrsLiveSourceManager();
4.SrsRtmpConn::publishing(),这个函数内部其实会阻塞的,里面会创建一个协程来处理后续的音视频推流,然后主协程循环统计信息。
5.rtrd->start():SrsRtmpConn::do_publishing 这个函数是重中之中,变量 rtrd 的创建代码如下:
SrsPublishRecvThread rtrd(rtmp, req, srs_netfd_fileno(stfd), 0, this, source, _srs_context->get_id());--:SrsRtmpConn::publishing
创建协程的地方,我截图贴出来:
我们知道,每次才创建协程之后,后面协程都是在 cycle() 函数跑起来的,所以 SrsRecvThread::cycle() 函数就是真正处理音视频推流的地方。
总结,本文其实只有3个重点。
1.推流的前期交互,创建流之类。
2.开一个协程函数 SrsRecvThread::cycle() 来处理客户端的音视频数据流推送。
3.主协程不断循环,统计流数据--SrsRtmpConn::do_publishing。
2.6.SrsRecvThread::cycle
参考:https://www.xianwaizhiyin.net/?p=1452
从上小节可知,真正接受客户端音视频流数据的地方是 SrsRecvThread::cycle() 。
那客户端推视频流来之后,服务器有没缓存?服务器缓存多少秒?怎么配置 SRS 让 RTMP 直播的延迟降低?
1. SrsRecvThread::cycle()
(1)pumper->on_start();
上面的变量 pumper 是 SrsPublishRecvThread,所以 on_start() 是指 SrsPublishRecvThread 的 on_start() 。
里面的函数并没有执行。
(2) if ((err = do_cycle()) != srs_success)
SrsRecvThread::cycle() 是一个协程函数,里面的重点是 do_cycle(),接下来分析 SrsRecvThread::do_cycle() 函数
2. SrsRecvThread::do_cycle()
(1)最重要的是下面两行代码。
// Process the received message.
if ((err = rtmp->recv_message(&msg)) == srs_success) {
err = pumper->consume(msg);
}
读取 RTMP 消息,然后丢给 pumper 处理,之前说过 pumper 是 SrsPublishRecvThread,在这里,大部分的 RTM消息都是音频帧或者视频帧。这里拿到的 RTMP 消息已经是由多个 chunk 拼接成一个完整的视频帧了。
(2)第一帧视频截图
(3)设置打印不受限制
set print elements 0
3.SrsPublishRecvThread::consume()
1)统计 video_frames,代码如下:
if (msg->header.is_video()) {
video_frames++;
}
2)把 RTMP 消息丢给 _conn 处理,代码如下:
err = _conn->handle_publish_message(_source, msg);--4
3)最后使用了一下 srs_thread_yield()。
4. SrsRtmpConn::handle_publish_message()
_conn->handle_publish_message() 函数的内部逻辑,这里的 _conn 是 SrsRtmpConn,
(1)处理 AMF 类型 的 RTMP消息。-- rtmp->decode_message-- SrsRtmpServer::decode_message-- SrsProtocol::decode_message-- SrsProtocol::do_decode_message
(2)用 process_publish_message() 处理视频、音频的 RTMP消息。
5.SrsRtmpConn::process_publish_message
(1)处理 MetaData 数据。如@setDataFrame。-- rtmp->decode_message
(2)处理音频数据。
(3)处理视频数据。--6
6. SrsLiveSource::on_video()
(1)检测视频帧的时间戳是不是递增的,检查RTMP头有没问题。
(2)调 on_video_imp() 处理视频帧。
到这里,整体的函数调用链条有点长,先画个流程图便于理解:
6. SrsLiveSource::on_video_imp
(1)对 sequence_header 的处理。sequence_header 可以理解为 H264 网络包的一个头。具体定义在 标准⽂档《ISO-14496-15 AVC file format》,搜索 AVCDecoderConfigurationRecord 就行。
(2)hub->on_video() 就是 SrsOriginHub::on_video()--format->on_video-- SrsRtmpFormat::on_video-- SrsFormat::on_video,主要把 H264包数据,解析到 两个变量 SrsVideoFrame* video 跟SrsVideoCodecConfig* vcodec。
(3)bridger_->on_video() 就是 SrsRtcFromRtmpBridger::on_video(),这个主要是一个桥接转换。RTMP转SRT、RTC 的,不用管。
(4)consumer->enqueue() 是 SrsLiveConsumer::enqueue(),这个是重中之重,会把 H264 视频帧插入队列,然后如果达到 350000 毫秒就通过条件变量,通知播放协程来取数据。
到这里,我们已经找到了,服务器缓存 视频的地方,服务器缓存视频默认是 350000,这个值应该可以在配置文件设置。
7. SrsLiveConsumer::wait
SrsRtmpConn::stream_service_cycle –》SrsRtmpConn::playing-- consumer->wait(mw_msgs, mw_sleep);
mw_min_msgs = nb_msgs;//8
mw_duration = msgs_duration;//3500
2.7.推流总结
参考:https://blog.csdn.net/u012117034/article/details/124122946
整个推流流程图太大,不便显示,请在见原链接查看。
1.SRS 服务器启动之后,会开启一个协程 (SrsTcpListener::cycle) 来 监听 1935 的RTMP端口,不断 accept 客户端请求。
2.始祖协程利用wg.wait() 等等其他的服务结束,其他服务是指 RTMP服务、SRT服务、RTC 服务。
3.有RTMP推流客户端请求来了,新开一个协程D(SrsRtmpConn::cycle)来处理 请求,包括RTMP握手,传输音视频数据前的交互。处理完前期的交互工作之后,发现客户端是一个推流客户端,就会再开一个协程 E(SrsRecvThread::cycle)来处理推过来的音视频数据流。之前的协程D 会阻塞不断循环统计一些信息。
4.所经历函数对比
视频 | 音频 | meta_data |
SrsRtmpConn::handle_publish_message | ||
SrsRtmpConn::process_publish_message | ||
SrsLiveSource::on_video (if (!mix_correct)) | SrsLiveSource::on_audio (if (!mix_correct)) | msg->header.is_amf0_data() meta_data为这个类型 |
SrsLiveSource::on_video_imp | SrsLiveSource::on_audio_imp | SrsRtmpServer::decode_message SrsProtocol::decode_message SrsProtocol::do_decode_message SrsOnMetaDataPacket::decode 存入到SrsAmf0Object* metadata—》SrsPacket包里的一个变量 |
SrsOriginHub::on_video | SrsOriginHub::on_audio | |
SrsRtmpFormat::on_video | SrsRtmpFormat::on_audio | |
SrsFormat::on_video | SrsFormat::on_audio | |
SrsFormat::video_avc_demux | SrsFormat::audio_aac_demux | |
(1avc_demux_sps_pp(2SrsFormat::video_nalu_demux | (1)audio_aac_sequence_header_demux (2) SrsFrame::add_sample | |
….参见在代码中的注释 | ….参见在代码中的注释 | ….参见在代码中的注释 |
最后音视频数据插入 SrsSample samples[SrsMaxNbSamples] | source->on_meta_data给各个consumer |
5. RTMP服务模块的启动、监听服务端口的处理流程,过程中涉及的关键类和关键函数如下图所示:
6.服务模块与推流相关的代码处理逻辑
3.结合代码分析握手协议
3.1.复杂握手协议与简单握手协议对比
1. rtmp 1.0规范中,指定了RTMP的握手协议:
- c0/s0:一个字节,一个字节的版本号。
- c1/s1: 1536字节,4字节时间,4字节0x00,1528字节随机数
- c2/s2: 1536字节,4字节时间1,4字节时间2,1528随机数和s1相同。
2.这个就是srs以及其他开源软件的simple handshake,简单握手,标准握手,FMLE也是使用这个握手协议。
3.Flash播放器连接服务器时,如果服务器只支持简单握手,则无法播放h264和aac的流,有数据,但没有视频和声音。
- 原因是adobe变更了握手的数据结构,标准rtmp协议的握手的包是随机的1536字节(S1S2C1C2),变更后的是需要进行摘要和加密。
- adobe将简单握手改为了有一系列加密算法的复杂握手(complex handshake)
4.simple简单握手和comple x复杂握手的主要区别:
SRS编译时若打开了SSL选项(--with-ssl),SRS会先使用复杂握手和客户端握手,若复杂握手失败,则尝试简单握手。
3.1.1. C0和S0格式(1 byte)
无论复杂握手协议和简单握手协议,都为一个字节:RTMP的版本,一般为3。
3.1.2. C1 和 S1(1536 bytes)
3.1.2.1简单握手协议中的C1和S1
time(4 bytes):本字段包含一个发送时间戳(取值可以为零或其他任意值)。客户端应该使用此字段来标识所有流块的时间戳。为了同步多个块流,客户端可能希望多个块流使用相同的时间戳。
zero(4 bytes):本字段必须全为零。S1这4个字节为C1的时间。
random (1528 bytes):本字段可以包含任何值。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没有必要为此使用加密数据或动态数据。
3.1.2.2复杂握手协议中的C1和S1
- time(4 bytes):发送的时间戳
- version (4 bytes):版本号
- 客户端的C1一般是0x80000702
- 服务端的S1一般是0x04050001、0x0d0e0a0d(livego中数值)
- key (764 bytes):结构如下
- random-data: (offset) bytes
- key-data: 128 bytes
- random-data: (764 - offset - 128 - 4) bytes
- offset: 4 bytes
- digest (764 bytes):结构如下
- offset: 4 bytes
- random-data: (offset) bytes
- digest-data: 32 bytes
- random-data: (764 - 4 - offset - 32) bytes
在不同的包里,key和digest顺序可能会颠倒,比如nginx-rtmp。3.1.3. C2 和 S2(1536 bytes)
3.1.3.1. 简单握手协议中的C2和S2
- time(4 bytes):本字段表示对端发送的时间戳(对C2来说是S1 ,对S2来说是C1)。
- time2(4 bytes):本字段表示接收对端发送过来的握手包的时间戳。
- random(1528 bytes):本字段包含对端发送过来的随机数据(对C2来说是S1,对S2来说是C1)。
握手的双方可以使用time和time2字段来估算网络连接的带宽和或延迟,但是不一定有用。
3.1.3.2. 复杂握手协议中的C2和S2
- random-data和digest-data都应来自对应的数据(对C2来说是S1,对S2来说是C1)
3.2.握手协议的代码实现
. RTMP 的握手逻辑全部在 rtmp->handshake() (即SrsRtmpServer::handshake())里面,SRS 的 RTMP 服务器实现,是先尝试复杂握手,不行再切换成简单握手。
3.2.1.复杂握手协议
1. SrsComplexHandshake::handshake_with_client
在SrsRtmpServer::handshake()中调用complex_hs.handshake_with_client(hs_bytes, io)。
2. SrsHandshakeBytes::read_c0c1
在SrsComplexHandshake::handshake_with_client中调用hs_bytes->read_c0c1(io)
- io->read_fully执行的代码为SrsTcpConnection::read_fully。再往下执行可以点击查看,不再详述。执行完以后将从网络中得到的数据存入c0c1变量中。
- if (uint8_t(c0c1[0]) == 0xF3)处理的为代理模式下的数据。具体协议参照:https://github.com/ossrs/go-oryx/wiki/RtmpProxy。
- 试着用两种模式对读取的数据进行解析。(3、4)
3.c1s1::parse
if (schema == srs_schema0) {
payload = new c1s1_strategy_schema0();--4
} else {
payload = new c1s1_strategy_schema1();
}
4. c1s1_strategy_schema0::parse
对key数据和digest数据进行分析。先解释key的数据。
key.parse(&stream))--5
5. key_block::parse(SrsBuffer* stream)
- 764个key数据的结构
- 代码的解释
- 764的最后4个字节(760-763)为offset,offset的数值通过key_block::calc_valid_offset()计算,即4个字节数相加,除以random-data的最大值的余数。
- 可以非常明白看到各个数值存到各个key的各个变量中。
6. digest_block::parse(SrsBuffer* stream)
执行完5所示的代码后回到步骤4,执行759的代码,调用了digest_block::parse。
- digest数据的结构
- 代码
和key数据的解析相似,将对应的数据存到digest变量中。
7.对digest的验证
执行完6以后,回到步骤1-- c1.c1_validate_digest。通过对digest数据的验证来验证是否是复杂的的握手数据。其步骤是:通过模式1得到的数据,验证digest数据,验证失败—>通过模式2再次得到的数据—>验证digest数据,仍旧验证失败—>复杂握手协议结束—>回到SrsRtmpServer::handshake()开始执行简单握手协议。
digest的具体验证没有看到相关的文档,具体的代码不再解析。
=========下面的分析为如果是复杂握手协议的交互===================
8. c1s1::s1_create
- 回到步骤1中的函数调用。前4个字节为时间,接着的4个字节为版本号,版本号注意写时为小端字节。
- 无论key和digest是用模式0或模式1,都将调用c1s1_strategy::s1_create函数。
9. c1s1_strategy::s1_create
- 先产生128个字节的key。
- 再调用calc_s1_digest产生digest。
- 在calc_s1_digest中调用 c1s1_strategy_schema0或c1s1_strategy_schema1进行组合。
10.执行过步骤9以后,回到步骤1,调用 s1.s1_validate_digest对生成的要进行验证。
11. 在步骤1中,按照格式生成S2,并进行验证。
12. 在步骤1中,生成s0s1s2,组合数据发送给客户端。
13. 在步骤1中,接收C2并解析。
3.2.2.简单握手协议
. 尝试复杂握手,切换成简单握手。
1.SrsSimpleHandshake::handshake_with_client--2
在这个函数中调用其他函数,完成简单握手过程。
2.SrsHandshakeBytes::read_c0c1
读取c0c1的值和复杂握手协议的相同。
3.根据hs_bytes->c0c1[0] != 0x03判断c0是否正确。
4.SrsHandshakeBytes::create_s0s1s2(const char* c1)
在步骤1中调用hs_bytes->create_s0s1s2(hs_bytes->c0c1 + 1)即执行这个函数。
- stream.write_1bytes(0x03);加个版本号
- stream.write_4bytes((int32_t)::time(NULL));加时间
- stream.write_bytes(c0c1 + 1, 4);将C1的时间加上(s1 time2 copy from c1)
- memcpy(s0s1s2 + 1537, c1, 1536);
- 9-1537的值为随机
- S2的值为C1
5.回到步骤1,然后hs_bytes->read_c2读取C2,没有解析。
6.从这里分析,s0、s1、s2 格式:
s0: 1 byte,version,为 0x03
s1:
- time:4 bytes,当前时间
- time2:4 bytes,拷贝自接收到的 c1 的开始 4 字节 time
- 余下随机数
s2: 完全拷贝自 c1 数据
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!