SRS总结 - 1

发布时间 2023-10-08 11:33:16作者: 泽良_小涛

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() 函数的流程图

https://www.xianwaizhiyin.net/wp-content/uploads/2022/03/srs-5-2.png

main()函数的内部逻辑实际上比较简单,因为所有的操作都封装在其他函数里面。特别是srs_thread_initialize()跟run_directly_or_daemon()函数。

  1. srs_thread_initialize:Initialize global or thread-local variables.
  2. 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服务

  1. main—》do_main—》run_directly_or_daemon—》run_hybrid_server—》SrsServerAdapter::SrsServerAdapter()—》srs = new SrsServer();
  2. 默认注册了两个服务:

_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,其他的执行了

https://img-blog.csdnimg.cn/img_convert/07e8b51c0b10cc2f26f452fd79e03293.png

(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 函数就行。为了便于理解,先画个结构图,如下:

https://img-blog.csdnimg.cn/img_convert/791d1ca5b729e59c1f524286dfcd8fcb.png

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(),这个链路有点长,如下:

https://img-blog.csdnimg.cn/img_convert/f38a9c66cf9a40ba119e58531ded2edf.png

所以重点 在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)

这个函数的调用链还是有点长,我还是画个流程图吧。

https://img-blog.csdnimg.cn/img_convert/73d53f8d0e102a691e48bae2a0d9b700.png

上图最重要的其实是 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,就是对象自己至此,虽然还有一点东西没讲,但是整体的流程图已经可以画出来了,如下:

https://img-blog.csdnimg.cn/img_convert/3760eb0aa7791573403f8e743fbffcd3.png

从上图可以看出,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 的入口,这个链条有点长,画个流程图便于理解。

https://img-blog.csdnimg.cn/img_convert/2cb4af979f7a419133b0aded34270ca6.png

到这里,已经找到 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响应,以及接收音视频流数据等处理。

https://www.xianwaizhiyin.net/wp-content/uploads/2022/03/srs-8-1-1024x893.png

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

https://www.xianwaizhiyin.net/wp-content/uploads/2022/03/srs-9-1-1024x509.png

1.这里插个题, 在调 stream_service_cycle() 之前,调了 trd->pull(), trd->pull() 在很多地方都出现,应该是个重点函数,具体另起一篇文章分析。本文暂时跳过。stream_service_cycle() 开头有一些 RTMP edge集群的逻辑,这块先跳过不管,本文环境没配置集群,不会跑进去那块逻辑。

2.因为 info->type 等于 SrsRtmpConnFMLEPublish,所以执行的rtmp->start_fmle_publish() ,这个函数是做推流交互处理的,fmle 是什么缩写我也不太清楚,埋个坑,后面填。下图 wireshark 圈出来的交互部分就是这个函数做的。

https://www.xianwaizhiyin.net/wp-content/uploads/2022/03/srs-9-2.png

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() 处理视频帧。

到这里,整体的函数调用链条有点长,先画个流程图便于理解:

https://www.xianwaizhiyin.net/wp-content/uploads/2022/03/srs-10-8.png

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的流,有数据,但没有视频和声音。

    1. 原因是adobe变更了握手的数据结构,标准rtmp协议的握手的包是随机的1536字节(S1S2C1C2),变更后的是需要进行摘要和加密。
    2. adobe将简单握手改为了有一系列加密算法的复杂握手(complex handshake)

4.simple简单握手和comple x复杂握手的主要区别:https://cdn.nlark.com/yuque/0/2022/png/2544508/1644224322109-48bf099e-d56e-4ba5-9afc-91b3bd8e1e23.png

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 数据