SRS总结(1)

发布时间 2023-09-20 15:31:51作者: 泽良_小涛

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.推流

1.5.2.拉流

1.5.3 sps、pps

1.5.4.video

1.5.5.audio

标准查询:

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

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

4.RTMP推流到SRS流媒体服务器消息处理

参考:https://www.yuque.com/wahaha-0yfyj/mnfloz/gm3skg?#

https://www.cnblogs.com/jimodetiantang/p/9072455.html

握手成功后,SRS和客户端进行消息交换,对应wiresharek这部分截图:

image.png

流程图(文件夹中:srs和客户端建联流程,从connect开始.png)

4.1. connect消息

4.1.1截图

4.1.2. 客户端生成connect消息发送到SRS服务器

对应FFmpeg代码:

gen_connect(s, rt)

static int gen_connect(URLContext *s, RTMPContext *rt)

//部分代码如下:

ff_amf_write_string(&p, "connect");

ff_amf_write_number(&p, ++rt->nb_invokes);

ff_amf_write_object_start(&p);

ff_amf_write_field_name(&p, "app");

ff_amf_write_string2(&p, rt->app, rt->auth_params);

4.1.3. SRS服务端接收connect消息并解析

1. SrsRtmpConn::do_cycle()

2.p (SrsRequest* req)

//解析connect消息,从流式数据->chunk->message->packet

3. SrsProtocol::decode_message—>SrsProtocol::do_decode_message

从2步骤如何到3

srs_error_t expect_message(SrsCommonMessage** pmsg, T** ppacket)

if ((err = decode_message(msg, &packet)) != srs_success)

4.在do_decode_message中解析packet代码。

5. SrsRtmpServer: expect_message

位于srs_rtmp_stack.hpp。
这是一个定义在 SrsRtmpServer 类中的模板函数。该函数指定了一个期待接收的消息,若接收到的是其他消息则丢弃,直到获取到指定的消息为止。

接着调用 SrsProtocol 类定义的同名模板函数 expect_message.

在分析 SrsProtocol: expect_message 之前,先看 SrsRtmpServer 类中类型为 SrsProtocol 类的成员 protocol 是如何构造的。

6. SrsProtocol 的构造函数

位于 srs_rtmp_stack.hpp。

SrsProtocol 类提供了 RTMP 消息协议服务,从 RTMP chunk stream 中接收 RTMP 消息,或者通过 RTMP chunk stream 发送 RTMP 消息。

7. SrsFastBuffer 类为协议提供字节缓存,将从 socket 接收到的数据存放到该类中,然后解码为 RTMP 消息。

8.得到一些连接信息,回复没有找到。

4.2. Window Acknowledgement Size消息

SRS服务器发送Window Acknowledgement Size消息给客户端,默认250000。250000从配置文件得到out_ack_size,没有配置文件则用默认值250000。ZLMediaKit设置为sendAcknowledgementSize(5000000);

4.2.1. Window Acknowledgement Size消息解析

1.wiresharek截图

https://cdn.nlark.com/yuque/0/2022/png/2544508/1642682367823-a271d101-3fbb-4ae7-9997-060f41fa9ef2.png

2.Type Id:0x5 对应ffmepg的RTMP_PT_WINDOW_ACK_SIZE

3.Window Acknowledgement Size⽤于设置窗⼝确认⼤⼩,⽐如这⾥是服务器发送给客户端的,当客户端收到该size就要Acknowledgement (0x3)。

4.对于ffmpeg,在接收到Window Acknowledgement Size的⼀半后发送确认包(Acknowledgement),以确保对等⽅可以继续发送⽽不等待确认。

5.注意点:

  • 客户端作为推流端时,⼀般即使没有收到服务器的ack,客户端也不会停⽌码流的推送。
  • 当客户端作为拉流端时,⼀般即使拉流端不回应ack,服务器也不会停⽌码流的发送。
  • 但彼此如果作为接收⽅时,收到1/2Windows size的数据后对会ack对⽅。

4.2.2. Window Acknowledgement Size消息代码分析

SrsRtmpConn::service_cycle()---rtmp->set_window_ack_size(out_ack_size)

SrsRtmpServer::set_window_ack_size(int ack_size)-- protocol->send_and_free_packet(pkt, 0)

SrsProtocol::send_and_free_packet--do_send_and_free_packet

1.rtmp header如何来?

do_send_and_free_packet--packet->to_msg-- shared_msg->create—》SrsSharedPtrMessage::create

4.3. Peer Bandiwdth消息

srs服务器发送Peer Bandiwdth消息给客户端,默认2500000。

4.3.1. Peer Bandiwdth消息解析

1.wiresharek截图:

https://cdn.nlark.com/yuque/0/2022/png/2544508/1642682560027-b5d08673-e716-4289-91a8-e9515ad7265b.png

2.客户端或服务器端发送此消息更新对端的输出带宽,作用是限制对端的输出带宽。和Window Acknowledgement Size相⽐,重点是更新。

3.Type ID:0x6 对应ffmpeg的RTMP_PT_SET_PEER_BW

4.如果消息中的Window ACK Size与上⼀次发送给发送端的size不同的话要回馈⼀个Window Acknowledgement Size的控制消息

image.png

  • Hard(Limit Type=0):接收端应该将Window Ack Size设置为消息中的值
  • Soft(Limit Type=1):接收端可以将Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size⼩与该控制消息中的Window Ack Size)
  • Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size

5. 与Window Acknowledgement Size相比,只是多了个Limit Type

4.3.2.Peer Bandiwdth消息代码分析

流程同Window Acknowledgement Size,

SrsRtmpConn::service_cycle()---rtmp->set_peer_bandwidth((int)(2.5 * 1000 * 1000), 2))

SrsRtmpServer:: set_peer_bandwidth-- protocol->send_and_free_packet(pkt, 0)

SrsProtocol::send_and_free_packet--do_send_and_free_packet

4.4. set Chunk Size消息

srs服务器发送Set Chunk Size消息给客户端

4.4.1. Set Chunk Size消息解析

1.wiresharek截图:

https://cdn.nlark.com/yuque/0/2022/png/2544508/1644310775870-7d230c9d-ac2c-4960-88c5-ec4a2429e0d6.png

2.Set Chunk Size(Message Type ID=1):设置chunk中Data字段所能承载的最⼤字节数,默认为128B,通信过程中可以通过发送该消息来设置chunk Size的⼤⼩(不得⼩于128B),⽽且通信双⽅会各⾃维护⼀个chunkSize,两端的chunkSize是独⽴的。

  • ⽐如当A想向B发送⼀个200B的Message,但默认的chunkSize是128B,因此就要将该消息拆分为Data分别为128B和72B的两个chunk发送,如果此时先发送⼀个设置chunkSize为256B的消息,再发送Data为200B的chunk,本地不再划分Message,B接受到Set Chunk Size的协议控制消息时会调整的接受的chunk的Data的⼤⼩,也不⽤再将两个chunk组成为⼀个Message。
  • 在实际中⼀般会把chunk size设置的很⼤,有的会设置为4096,FFMPEG推流的时候设置的是 60*1000,这样设置的好处是避免了频繁的拆包组包,占⽤过多的CPU。

3.以下为代表Set Chunk Size消息的chunk的Data:

image.png

其中第⼀位必须为0,chunk Size占31个位,最⼤可代表2147483647=0x7FFFFFFF -1,但实际上所有⼤于16777215=0xFFFFFF的值都⽤不上,因为chunk size不能⼤于Message的度,表示Message的长度字段是⽤3个字节表示的,最⼤只能为0xFFFFFF。

在代码里是从配置文件中设置的。没有配置时返回#define SRS_CONSTS_RTMP_SRS_CHUNK_SIZE 60000。

4.4.2. Set Chunk Size消息代码分析

流程同Window Acknowledgement Size和Peer Bandiwdth

SrsRtmpConn::service_cycle()---rtmp->set_chunk_size(chunk_size)

SrsRtmpServer:: set_chunk_size-- protocol->send_and_free_packet(pkt, 0)

SrsProtocol::send_and_free_packet--do_send_and_free_packet

4.5.SRS服务器发送response_connect_app消息给客户端

SRS服务器是会发送response_connect_app消息给客户端用来响应客户端发送的connect消息,但这部分wiresharek是没有抓取到此包的。

通过tcpdump命令查看包信息:

sudo tcpdump -i any port 1935 -XX and dst 36.112.32.2 #不是公网地址

流程同Window Acknowledgement Size、Peer Bandiwdth和Set Chunk Size

SrsRtmpConn::service_cycle()---rtmp->response_connect_app

SrsRtmpServer::response_connect_app-- protocol->send_and_free_packet(pkt, 0)

SrsProtocol::send_and_free_packet--do_send_and_free_packet

4.6.客户端发送releaseStream、FCPublish、CreateStream和checkbw消息给SRS服务器

客户端还需要发送 releaseStream , FCPublish,CreateStream和checkbw消息给SRS服务器,具体可以参考ffmpeg的handle_invoke_result函数和gen_check_bw函数。

4.6.1 releaseStream消息

客户端发送releaseStream消息给srs服务器。

对应的值liveStream是要处理的流,作用是生成'releaseStream'调用并发送到服务器,让服务器为媒体流释放一些通道。

image.png

1.客户端生成releaseStream消息发送给SRS服务端,函数为int gen_release_stream (URLContext *s, RTMPContext *rt)。

2.SRS服务端接收到后会解析成对应的packet,然后返回_result消息。如下wiresharek所示

3.SRS服务器解析releaseStream消息代码:

do_decode_message(SrsMessageHeader& header, SrsBuffer* stream, SrsPacket** ppacket)

else if (command == RTMP_AMF0_COMMAND_FC_PUBLISH) {

*ppacket = packet = new SrsFMLEStartPacket();

return packet->decode(stream);

}

4.根据packet类型返回对应响应,ReleaseStream/PublishStream/FCPublish/FCUnpublish都是用SrsRtmpServer::identify_fmle_publish_client函数返回_result消息。

4.6.2客户端发送FCPublish消息给srs服务器

1.FCPublish消息作用是使服务器为接收媒体流做好准备。

  1. 客户端生成FCPublish消息发送给SRS服务器 ,函数为int gen_fcpublish_stream(URLContext *s, RTMPContext *rt)
  2. SRS服务器解析FCPublish消息:

4. 调用栈:

FCPublish、createStream、publish、onFCPublish、onStatus都是通过SrsRtmpServer::start_fmle_publish函数进行处理

5.SRS服务器发送_result消息给客户端进行响应FCPublish消息,见如上代码:SrsRtmpServer::identify_fmle_publish_client

4.7客户端发送CreateStream消息给srs服务器

4.7.1协议解析

1.Create Stream:创建传递具体信息的通道,从⽽可以在这个流中传递具体信息,传输信息单元为Chunk。

2.当发送完createStream消息之后,解析服务器返回的消息会得到⼀个stream ID,这个ID也就是以后和服务器通信的 message stream ID,⼀般返回的是1,不固定。

4.7.2. CreateStream消息代码解析

1.客户端生成CreateStream消息发送给SRS服务器,函数为
static int gen_create_stream(URLContext *s, RTMPContext *rt)。

2.SRS服务器解析CreateStream消息:

else if (command == RTMP_AMF0_COMMAND_CREATE_STREAM)

  1. SRS服务器发送_result消息给客户端进行响应CreateStream消息,见如上代码:SrsRtmpServer::identify_fmle_publish_client
  2. 大致流程

SrsRtmpConn::service_cycle()?SrsRtmpConn::stream_service_cycle? SrsRtmpServer::start_fmle_publish(expect_message<SrsCreateStreamPacket>)? srs_rtmp_stack.hpp: expect_message? SrsProtocol::decode_message? SrsProtocol::do_decode_message? packet = new SrsCreateStreamPacket(); ? SrsRtmpServer::start_fmle_publish(SrsCreateStreamResPacket* pkt = new SrsCreateStreamResPacket)

4.8服务器向客户端发送onBWDone消息

在客户端向服务器端发送CreateStream消息后,服务器向客户端发送onBWDone消息。只在SrsOnBWDonePacket构造函数中赋值。当带宽测试完成后,通知客户端,实际代码中没有发现什么特殊代码。

SrsRtmpConn::service_cycle--rtmp->on_bw_done()—》SrsRtmpServer::on_bw_done--protocol->send_and_free_packet(pkt, 0)

4. 9客户端发送checkbw消息给srs服务器

1.客户端生成检查带宽消息发送给服务器。

客户端生成checkbw消息发送给SRS服务器,客户端ffmpeg函数 gen_check_bw。

2.执行过程过程

SrsRtmpConn::cycle—service_cycle()—》SrsRtmpConn::service_cycle-- stream_service_cycle()—》

SrsRtmpConn::stream_service_cycle()--rtmp->start_fmle_publish(info->res->stream_id)—》SrsRtmpServer::start_fmle_publish-- expect_message<SrsPublishPacket>(&msg, &pkt)—》SrsRtmpServer- expect_message—》SrsProtocol- expect_message-- recv_message(&msg)? SrsProtocol::recv_message-- on_recv_message(msg)? SrsProtocol::on_recv_message? switch (msg->header.message_type)—default? SrsProtocol- expect_message-( recv_message执行完) decode_message—》do_decode_message-- ppacket = packet = new SrsCallPacket()?SrsCallPacket::decode—对各字段进解析,没做更多的工作。

3.根据分析的过程,只是行行了解析,没有进行过多的操作。

4.10.publish消息

推流客户端使用publish消息向SRS服务器端发布一个命名的流,发布之后,任意客户端都可以通过该名称请求视频、音频和数据。

1.客户端生成publish消息并发送到SRS服务器,函数为int gen_publish(URLContext *s, RTMPContext *rt)。

2. SRS服务器解析publish消息:

在SrsProtocol::do_decode_message

3. 调用栈

4.解析成功后SRS服务器会生成onFCPublish消息返回给客户端。

4.11. onFCPublish消息解析

SRS服务器向客户端发送onFCPublish消息

1. onFCPublish消息是回应publish消息。

2. SRS服务器生成onFCPublish消息并发送到客户端:

在SrsRtmpServer::start_fmle_publish(int stream_id)函数中,收到publis消息后,进行回复。

4.12. onStatue消息解析

1.SRS服务器还会生成onStatus消息向客户端发送,描述的状态内容中code为NetStream.Publish.Start,description为Start publishing,目的就是告诉推流客户端,现在可以推流了。

2. SRS服务器生成onStatue消息并发送到客户端:

在SrsRtmpServer::start_fmle_publish(int stream_id)函数中,收到publish消息后,进行回复。