写点什么

玩转直播系列之 RTMP 协议和源码解析(2)

发布于: 2021 年 05 月 17 日
玩转直播系列之RTMP协议和源码解析(2)

一、背景


实时消息传输协议(Real-Time Messaging Protocol)是目前直播的主要协议,是 Adobe 公司为 Flash 播放器和服务器之间提供音视频数据传输服务而设计的应用层私有协议。RTMP 协议是目前各大云厂商直线直播业务所公用的基本直播推拉流协议,随着国内直播行业的发展和 5G 时代的到来,对 RTMP 协议有基本的了解,也是我们程序员必须要掌握的基本技能。


本文主要阐述 RTMP 的基本思想和核心概念,并且辅之以 livego 的源码分析,和大家一起深入学习 RTMP 协议最核心的知识点。

二、RTMP 协议特点


RTMP 协议主要的特点有:多路复用,分包和应用层协议。以下将对这些特点进行详细的描述。

2.1 多路复用

多路复用(multiplex)指的是信号发送端通过一个信道同时传输多路信号,然后信号接收端将一个信道中传递过来的多个信号分别组合起来,分别形成独立完整的信号信息,以此来更加有效地使用通信线路。

简而言之,就是在一个 TCP 连接上,将需要传递的 Message 分成一个或者多个 Chunk,同一个 Message 的多个 Chunk 组成 ChunkStream,在接收端,再把 ChunkStream 中一个个 Chunk 组合起来就可以还原成一个完整的 Message,这就是多路复用的基本理念。

上图是一个简单例子,假设需要传递一个 300 字节长的 Message,我们可以将其拆分成 3 个 Chunk,每一个 Chunk 可以分成 Chunk Header 和 Chunk Data。在 Chunk Header 里我们可以标记这个 Chunk 中的一些基本信息,如 Chunk Stream Id 和 Message Type;Chunk Data 就是原始信息,上图中将 Message 分成 128+128+44 =300,这样就可以完整的传输这个 Message 了。


关于 Chunk Header 和 Chunk Data 的格式,后文会进行详细介绍。

2.2 分包

RTMP 协议的第二个大的特性就是分包,与 RTSP 协议相比,分包是 RTMP 的一个特点。与普通的业务应用层协议(如:RPC 协议)不一样的是,在多媒体网络传输案例中,绝大多数的多媒体传输的音频和视频的数据包都相对比较偏大,在 TCP 这种可靠的传输协议之上进行大的数据包传递,很有可能阻塞连接,导致优先级更高的信息无法传递,分包传输就是为了解决这个问题而出现的,具体的分包格式,下文会有介绍。

2.3 应用层协议

RTMP 最后的一个特性,就是应用层协议。RTMP 协议默认基于传输层协议 TCP 而实现,但是在 RTMP 的官方文档中,只给定了标准的数据传输格式说明和一些具体的协议格式说明,并没有具体官方的完整实现,这就催生出了很多相关的其他业内实现,例如 RTMP over UDP 等等相关的私有改编的协议出现,给了大家更多的可扩展的空间,方便大家解决原生 RTMP 存在的直播时延等问题。

三、RTMP 协议解析


作为一种应用层协议,和其他私有传输协议一样(如 RPC 协议),RTMP 也有一些具体代码实现,如 nginx-rtmp、livego 和 srs。本文选用基于 go 语言实现的开源直播服务器 livego 进行源码级的主流程分析,和大家一起深入学习 RTMP 推拉流的核心流程的实现,帮助大家对 RTMP 的协议有一个整体的理解。


在进行源码分析之前,我们会通过类比 RPC 协议的方式,帮助大家对 RTMP 协议的格式有一个基本的了解,首先我们可以看一个比较简单但实用的 RPC 协议格式,如下图所示:

我们可以看到这是一个在 RPC 调用过程中所使用的数据传输格式,之所以使用这样的格式,根本目的还是为了解决"粘包和拆包"的问题。


以下简要描述图中 RPC 协议的格式:首先用 2 个字节,MAGIC 来表示魔数,标记该协议是对端都能识别的标识,如果接收到的 2 个字节不是 0xbabe 的话,则直接丢弃该包;第二个 sign 占用 1 个字节,低 4 位表示消息的类型 request/response/heartbeat,高 4 位表示序列化类型例如 json,hessian,protobuf,kyro 等等;第三个 status 占用一个字节,表示状态位;随后使用 8 个字节来表示调用的 requestId,一般使用低 48 位(2 的 48 次方)就足够表示 requestId 了;接着使用 4 字节定长的 body size 来表示 Body Content,通过这样的方式就能够很快的解析出 RPC 消息 Message 的完整请求对象了。


通过分析上述的一个简单的 RPC 协议,其实我们能够发现一个很好的思想,就是最大效率的使用字节,即使用最小的字节数组,来传输最多的数据信息。小小的一个字节能够带来很多的信息量,毕竟一个字节它有 64 种不同的变化。在网络中,如果只需要利用一个字节就能够传递很多有用的信息的话,那么我们就可以使用极其有限的资源来得到最大的资源利用了。RTMP的官方文档在 2012 年就出现了,虽然以目前的眼光来看,RTMP 协议实现的非常复杂,甚至有些臃肿,但是它在 2012 年的时候,就能够有比较先进的思想,的确是我们学习的榜样。


在当今 WebRTC 协议横行的年代里,我们也能够从 WebRTC 的设计实现中,看到 RTMP 的影子,上述的 RPC 协议我们就可以认为是一个与 RTMP 具有相似设计理念的简化版设计。

3.1 RTMP 核心概念说明

在分析 RTMP 源码之前,我们先对 RTMP 协议中的几个核心概念做具体说明,方便我们在宏观上对 RTMP 整个协议栈有一个基本的了解,并且在后文源码分析期间,我们也会通过抓包的方式,更加直观地帮助我们去分析相关的原理。


首先,和刚才的 RPC 协议格式一样,RTMP 实际传输的实体对象是 Chunk,一个 Chunk 由 Chunk Header 和 Chunk Body 两个部分组成,如下图所示。

3.1.1Chunk Header

Chunk Header 这个部分和我们前面说过的 RPC 协议不太一样,主要是 RTMP 协议的 Chunk Header 的长度不是固定的,为什么不是固定的呢?其实还是 Adobe 公司为了节省数据传输开销。从刚才将一个 300 字节的 Message 拆分成 3 个 Chunk 的例子中,我们可以看到多路复用其实也是有一个比较明显的缺点,就是我们需要有一个 Chunk Header 来标记这个 Chunk 的基本信息,这样其实就是在传输的时候有了额外字节流传输的开销。所以为了保证传输的字节数最少,我们就需要不断地压榨着 RTMP 的 Header 的大小,确保 Header 的大小达到最小,这样才能达到最高的传输效率。


首先我们研究一下 Chunk Header 中 Basic Header 的部分,Basic Header 的长度就是不固定的,可以是 1 个字节,2 个字节或者 3 个字节,这取决于 Chunk Stream Id(缩写:csid)。


RTMP 协议支持的 csid 的范围是 2~65599,0 和 1 是协议保留值,用户不可使用。Basic Header 至少含有 1 个字节(低 8 位),它的长度就是这 1 个字节决定的,如下图所示。该字节高 2 位留给 fmt,fmt 的取值决定了 Message Header 的格式,这个在后面会讲到。该字节的低 6 位就是 csid 的值,当低 6 位的 csid 取值为 0 时,表示真实 csid 值大到无法用 6 个 bit 表示了,需要借助后续的一个字节才行;当低 6 位的 csid 取值为 1 时,表示真实 csid 值大到无法用 14 个 bit 表示了,需要再借助后续的一个字节才行。于是,整个 Basic Header 的长度看起来就不是固定的了,完全取决于首字节的低 6 位的 csid 的值。


实际应用中,并没有使用到那么多 csid,也就是说一般情况下,Basic Header 长度为一个字节,csid 取值范围为 2~63。

刚才说了那么多,才仅仅说了 Basic Header,而 Basci Header 只是 Chunk Header 的组成部分之一,比较喜欢折腾的 RTMP 协议的作者,把 RTMP 的 Chunk Header 模块又设计成了动态大小的,简而言之也是为了节省传输空间,这边能够方便理解的地方就是 Chunk Message Header 的长度也分四种情况,这就是前面提到的 fmt 这个值决定的。


Message Header 的四种格式如下图所示:

当 fmt 为 0 的时候,Message Header 占用 11 个字节(请注意,这边的 11 个字节不包括 Basic Header 的长度),由 3 个字节长度的 timestamp,3 个字节长度的 message length,1 个字节长度的 message type Id,4 个字节长度的 message stream Id 所组成的。


其中,timestamp 是绝对时间戳,表示的是这个消息发送的时间;message length 表示的是 chunk body 的长度;message type id 表示的是消息类型,这个在后文会具体讲到;message stream id 是消息唯一标识。这边需要注意的是,如果这个消息的绝对时间戳大于 0xFFFFFF,说明这个时间大到无法用 3 个字节来表示,需要借助扩展时间戳(Extended Timestamp)来表示,扩展时间戳长度为 4 个字节,默认放在 Chunk Header 和 Chunk Body 之间。

当 fmt 为 1 的时候,Message Header 占用 7 个字节,与之前的 11 个字节的 chunk header 相比,少了一个 message stream id,这个 chunk 是复用之前的 chunk stream id,这个一般用于可变长的消息结构。


当 fmt 为 2 的时候,Message Header 只占用 3 个字节,就只包含 timestamp 的三个字节,与之前相比,既少了 stream id 也少了 message length,这种少了 message length 的,一般用于固定长度但是需要修正时间的消息(如:音频数据)。


当 fmt 为 3 的时候,Chunk Header 里就不包含 Message Header 了。一般来说,在拆包的时候,把一个完整的 RTMP 的 Message 消息,会拆成第一个是 fmt 为 0 的 Chunk 消息,随后的消息也会拆成 fmt 为 3 的消息,这样的做的方式就是第一个 Chunk 附带着最全的 Chunk 消息信息,后续 Chunk 信息的 Header 就会比较小,这样实现比较简单,压缩率也是比较好。当然,如果第一个 Message 发送成功之后,第二个 Message 再次发送的时候,就会把第二个 Message 的第一个 Chunk 设置成 fmt 为 1 类型的 Chunk,随后该 Message 的 Chunk 的 fmt 为 3,这样就能够进行消息的区分。

3.1.2 Chunk Body

刚才花了很多时间去描述 Chunk Header,接下来我们再针对 Chunk Body 进行简单的描述。与 Chunk Header 相比,Chunk Body 就比较简单,没有那么多变长的控制,结构也比较简单,这个里面的数据也就是真正有业务含义的数据,长度默认是 128 个字节(可以通过 set chunk size 命令协商更改)。里面的数据包组织格式一般是 AMF 或者 FLV 格式的音视频数据(不含 FLV TAG 头)。AMF 组织结构的数据组成如下图所示,FLV 格式本文不做深入描述,感兴趣的话可以阅读 FLV 官方文档

3.1.3 AMF

AMF(Action Message Format) 是一种类似 JSON,XML 的二进制数据序列化格式,Adobe Flash 与远程服务端可通过 AMF 格式的数据进行数据通信。


AMF 具体的格式其实与 Map 的数据结构很相似,就是在 KV 键值对的基础上,中间多加了一个 Value 值的 length。AMF 的结果基本如下图所示,有时候 len 字段就是空,这个是由 type 来决定的,我们举例来说,例如我们传输的是 number 类型的 AMF 格式的数据,那么 len 字段我们就可以忽略,因为我们默认 number 类型的字段占用 8 个字节,我们这边就可以忽略了。


再举例来说,AMF 如果传输的是 0x02 string 类型的数据的时候,len 的长度就默认占据 2 个字节,因为 2 个字节足够表示后面 value 的最大长度了。以此类推,当然有些时候,len 和 value 的值都不存在,就比如传递 0x05 传递 null 的时候,len 和 value 我们就都不需要了。

以下列举一些常用的 AMF 的 type 的对应表格,更多信息可以查看官方文档


我们可以通过 WireShark 来抓包,实际来体验一下具体的 AMF0 的格式。

如上图所示,这是一个非常典型的 AMF0 类型 string 结构的抓包。AMF 目前有 2 个主要的版本,分别是 AFM0 和 AMF3,在目前的实际使用场景中,AMF0 还是占据主流的地位。那么 AMF0 和 AMF3 有什么区别呢,当客户端给服务器端发送 AMF 格式 Chunk Data 数据的时候,服务端在接收到该信息的时候,如何是知道 AMF0 或者是 AMF3 呢?实际上 RTMP 在 Chunk Header 中使用 message type id 来进行区分,当消息使用 AMF0 编码时,message type id 等于 20,使用 AMF3 编码时 message type id 等于 17。

3.1.4 Chunk & Message

首先,用一句话来总结一下 Chunk 和 Message 的关系,一个 Message 是由多个 Chunk 组成,多个 Chunk Stream id 一样的 Chunk 称之为 Chunk Stream,接收端可以重新合并解析为完整的 Message。RTMP 相比于 RPC 消息来说,消息类型多了很多,前文讲的 RPC 消息类型归根结底就 request,response 和 heartbeat 这三种类型,但是 RTMP 协议的消息类型就比较丰富。RTMP 消息主要分为以下三大类型:协议控制消息,数据消息和命令消息。


协议控制消息:Message Type ID = 1~6,主要用于协议内的控制。


数据消息:Message Type ID = 8 9


188: Audio 音频数据

9: Video 视频数据 1

8: Metadata 包括音视频编码、视频宽高等音视频元数据。


命令消息 Command Message (20, 17):此类型消息主要有 NetConnection 和 NetStream 两类,两类分别有多个函数,该消息的调用,可理解为远程函数调用。


总览图如下,后续在源码解析章节,会进行具体介绍,其中着色部分为常用消息。

3.2 核心实现流程

网络协议的学习是一个枯燥的过程,我们尝试结合 RTMP 协议原文和 WireShark 抓包的方式,尽量形象地给大家描述 RTMP 协议中的核心流程,包括握手,连接,createStream,推流和拉流。本节所有的抓包数据的基本环境是:livego 作为 RTMP 服务器(服务端口为 1935),OBS 作为推流应用,VLC 作为拉流应用。


作为一个应用层协议解析来说,首先,我们要注意的就是主体流程的把握,对于每一个 RTMP 服务器来说,每一个推流和拉流从代码层面来说,都是一个网络链接,针对每一个连接,我们要进行对应的工序进行处理,我们可以看到 livego 中源码中所展示的一样,有一个 handleConn 方法,顾名思义,就是用来处理每一个连接,按照主流程来说,分为第一部分的握手,第二个核心模块的依据 RTMP 包协议,进行 Chunk header 和 Chunk body 的解析,后续再根据解析出来的 Chunk header 和 Chunk body 再做具体的处理。


可以看到上述代码块,主要有 2 个核心方法:一个是 HandshakeServer,主要处理握手逻辑;另一个是 ReadMsg 方法,主要处理 Chunk header 和 Chunk body 信息的读取。

3.2.1 第一部分-握手(Handshake)

协议原文的 5.2.5 节详细介绍了 RTMP 握手的过程,图示如下:

乍一看,可能会觉得此过程有些复杂。所以,我们还是先用 WireShark 抓包来整体看看过程吧。


WireShark 抓包的 Info 能够为我们解读 RTMP 包的含义,从下图可以看出,握手主要涉及到 3 个包。其中第 16 号包是客户端向服务端发送 C0 和 C1 消息,18 号包是服务端向客户端发送 S0,S1 和 S2 消息,20 号包是客户端向服务端发送 C2 消息。如此,客户端和服务端就完成了握手过程。


通过 WireShark 抓包可以看出,握手过程还是非常简洁的,有点类似 TCP 三次握手的过程,所以从实际抓包来说,与 RTMP 协议原文的 5.2.5 节介绍的还是有些出入的,整体流程变得很简洁。

现在可以回头看看上面那个比较复杂的握手流程图了。图中将客户端和服务端分为四种状态,分别是:未初始化,已发送版本号,已发送 ACK,握手完成。


未初始化:客户端和服务端无任何交流阶段;

已发送版本号:发送了 C0 或者 S0;

已发送 ACK:发送了 C2 或者 S2;

握手完成:接收到了 S2 或者 C2。


RTMP 协议规范并没有限定死 C0,C1,C2 和 S0,S1,S2 的顺序,但是制定了以下规则:


客户端必须收到服务端发来的 S1 后才能发送 C2;

客户端必须收到服务端发来的 S2 后才能发送其他数据;

服务端必须收到客户端发来的 C0 后才能发送 S0 和 S1;

服务端必须收到客户端发来的 C1 后才能发送 S2;

服务端必须收到客户端发来的 C2 后才能发送其他数据。


从 WireShark 抓包分析可以看出,整个握手过程的确是遵循了以上规定。现在问题来了,C0,C1,C2,S0,S1 和 S2 这些消息到底是些什么玩意?其实,RTMP 协议规范里面明确定义了它们的数据格式。


C0 和 S0:1 个字节长度,该消息指定了 RTMP 版本号。取值范围 0~255,我们只需要知道 3 才是我们需要的就行。其他取值含义感兴趣的话可以阅读协议原文。


C1 和 S1:1536 个字节长度,由 时间戳+零值+随机数据 组成,握手过程的中间包。


C2 和 S2:1536 个字节长度,由 时间戳+时间戳 2+随机数据回传 组成,基本上是 C1 和 S1 的 echo 数据。一般在实现上,会令 S2 = C1,C2 = S1。


下面我们结合 livego 源码来加强对握手过程的理解。

到此为止,最简单的握手流程就到此结束了,可以看出整个握手流程还是比较清晰的,处理逻辑也是比较简单,也比较便于理解。

3.2.2 第二部分-信息交换

3.2.2.1 解析 RTMP 协议的 Chunk 信息

握手之后,就要做开始做连接等相关的事情处理了,再做此信息处理之前,工欲善其事必先利其器。


我们先要按照 RTMP 协议的规范来解析 Chunk Header 和 Chunk body 了,将网络传输的字节包数据转换成我们可识别的信息处理,再根据这些可识别的信息数据,再做对应流程的处理,这块是源码解析的关键核心,涉及的知识点非常多,大家可以结合上文一起看,可以方便大家理解 ReadMsg 这块核心逻辑的理解。


上述的代码块逻辑很清晰,主要是读取每一个 conn 连接中,进行对应的编解码,获取到一个个 Chunk,并且将相同 ChunkStreamId 的 Chunk 再次进行合并,合并成对应的 Chunk Stream,最后一个个完整的 Chunk Stream 就是 Message 了。


这块代码就是和我们之前理论部分知识介绍的 chunkstreamId 那块知识比较接近的地方了,大家可以结合起来一起看,大家在脑海中,要注意就是一个 conn 连接,会传递多个 Message,例如连接 Message,createStreamMessage 等等,每一个 Message 就是 Chunk Stream,也就是多个 csid 相同的 Chunk,所以 livego 的作者使用 map 这样的数据结构进行存储,key 就是 csid,value 就是 chunkstream,这样就可以将向 rtmp 服务器发送过来的信息能够全部保存下来。

readChunk 代码的具体逻辑实现分成如下几个部分:

1)csid 的修正,至于理论部分参照上述逻辑,这块其实是 basic header 的处理。

2)Chunk Header 按照 format 的数值进行对应的解析处理,上文理论部分也已经介绍过了,下文也有具体的注释解释,有两个技术点需要注意第一就是 timestramp 时间戳的处理,第二个注意点是 chunk.new(pool)这行代码,也是需要大家注意,代码注释中也写的比较清楚。

3)Chunk Body 的读取处理,上文理论部分说过,Chunk header 中当 fmt 为 0 的时候,会有一个 message length 字段,这个字段会控制 Chunk Body 的大小,依据这个字段,我们可以很轻松地读取到 Chunk body 信息的读取,整体逻辑如下。

到此为止,我们已经成功解析了 Chunk Header,读取了 Chunk Body,注意我们只是读取了 Chunk Body 还没有按照 AMF 格式对 Chunk Body 进行解析,针对 Chunk Body 部分的逻辑处理,在下文会进行详细的源码介绍,不过现在我们已经解析到了一个连接发送过来的 ChunkStream 了,接下来我们就可以回到主流程的分析了。


刚才说了握手完成后,并且我们也解析到了 ChunkStream 信息了,接下来我们就要依据 ChunkStream 的 typeId 和 Chunk Body 中的 AMF 数据进行对应的工序流程处理了,具体思路大家可以这样理解,客户端 A 发送 xxxCmd 命令,RTMP 服务端根据 typeId 和 AMF 信息解析出 xxxCmd 命令,并给以对应命令的响应。

上述代码块中的 handleCmdMsg 中也是这个 RTMP 服务端处理客户端命令的代码精髓了,可以看出 livego 是支持 AMF3 和 AMF0 的,AMF3 和 AMF0 的区别,上文也已经介绍过了,下文的代码注释写的也比较清楚,然后就是解析 AMF 格式的 Chunk Body 的数据,解析出来的结果也是按照 Slice 格式进行存储。

解析好 typeId 和 AMF,接下来就是水到渠成的对各个命令进行处理了。

接下来是针对每一个客户端命令的处理了。

3.2.2.2 连接

连接(Connect)命令处理过程:连接过程客户端和服务端会完成窗口大小,传输块大小和带宽大小的确认,RTMP 协议原文详细介绍了连接过程,如下图所示:

同样,我们这里用 WireShark 抓包分析:

从抓包可以看出,连接过程只用了 3 个包就完成了:


22 号包:客户端告诉服务端,我想要设置 chunk size 为 4096;


24 号包:客户端告诉服务端,我想要连接叫 “live” 的应用;


26 号包:服务端响应客户端的连接请求,确定窗口大小,带宽大小和 chunk size,以及返回 “_result” 表示响应成功。这些都是通过一个 TCP 包来完成的。


那么客户端和服务端是如何知道这些包的含义的呢?这就是 RTMP 协议规范所制定的规则了,我们可以通过阅读规范来了解,当然也可以通过 wrieshark 来帮助我们快速解析。以下是 22 号包的详细解析,我们重点关注 RTMP 协议解析信息就行。

从图中可以看出, RTMP Header 包含有 Format 信息,Chunk Stream ID 信息,Timestamp 信息,Body size 信息,Message Type ID 信息和 Messgae Stream ID 信息。Type ID 的十六进制值为 0x01,含义为 Set Chunk Size,属于协议控制消息(Protocol Control Messages)。


RTMP 协议规范 5.4 节规定了,对于协议控制消息,Chunk Stream ID 必须设为 2,Message Stream ID 必须设为 0,时间戳直接忽略。从 WireShark 抓包解析出的信息可知,22 号包的确是符合 RTMP 规范的。


现在我们来看看 24 号包的详细解析。

24 号包也是客户端发出的,可以看到它设置 Message Stream ID 为 0,Message Type ID 为 0x14(即十进制的 20),含义为 AMF0 命令。AMF0 属于 RTMP 命令消息(RTMP Command Messages),RTMP 协议规范并没有规定连接过程必须要使用的 Chunk Stream ID,因为真正起作用的是 Message Type ID,服务端根据 Message Type ID 来做相应的响应。连接过程发送的 AMF0 命令携带的是 Object 类型的数据,会告诉服务端要连接的应用名和播放地址等信息。


以下代码是 livego 处理客户端请求连接的过程。

收到客户端连接应用的请求后,服务端需要作出相应响应给客户端,也就是 WireShark 抓取的 26 号包的内容,详细内容如下图所示,可以看到服务端在一个包里面做了好几件事情。

我们可以结合 livego 源码来深入学习该过程。

3.2.2.3 createStream

连接完成后,就可以创建流了。创建流的过程相对来说比较简单,只需要两个包就能够实现,如下所示:

其中 32 号包是客户端发起 createStream 请求,34 号包是服务端响应,以下是 livego 处理客户端连接请求的源码。

3.2.2.4 推流

创建流完成后,就可以开始推流或者拉流了,RTMP 协议规范的 7.3.1 节也有给出推流示意图,如下图所示。其中连接和创建流的过程上文已经详细介绍过了,我们重点看发布内容(Publishing Content)的过程就行。

使用 livego 推流前,需要先获取推流的 channelkey。我们可以通过如下命令获取频道为 “movie” 的 channelKey。响应内容中的 Content 的 data 字段值就是推流需要的 channelKey。

$ curl http://localhost:8090/control/get?room=movie StatusCode        : 200StatusDescription : OKContent           : {"status":200,"data":"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575K                    LkIZ9PYk"}RawContent        : HTTP/1.1 200 OK                    Content-Length: 72                    Content-Type: application/json                    Date: Tue, 09 Feb 2021 09:19:34 GMT                     {"status":200,"data":"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575K                    LkIZ9PYk"}Forms             : {}Headers           : {[Content-Length, 72], [Content-Type, application/json], [Date                    , Tue, 09 Feb 2021 09:19:34 GMT]}Images            : {}InputFields       : {}Links             : {}ParsedHtml        : mshtml.HTMLDocumentClassRawContentLength  : 72
复制代码

使用 OBS 推流到 livego 服务器中应用名为 live 的 movie 频道,推流地址为:rtmp://localhost:1935/live/rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk。同样,我们还是先看一下 WireShark 的抓包内容吧。

推流初期,客户端发起 publish 请求,也就是 36 号包的内容,该请求中需要带上频道名,在这个包里面就是"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"。


服务端会首先会检测这个频道名是否存在以及检查这个推流名是否被使用中,如果不存在或者在使用的话就会拒绝客户端的推流请求。由于我们在推流前已经生成了该频道名,客户端可以合法使用,于是服务端在 38 号包中回应的是 "NetStream.Publish.Start",也就是告诉客户端可以开始推流了。客户端在推流音视频数据前需要先把音视频的的元数据发给服务端,也就是 40 号包所做的事情,我们可以看一下该包的详细内容。从下图可以看出,发送元数据信息比较多,包含有视频分辨率,帧率,音频采样率和音频声道等关键信息。

告诉服务端音视频元数据后,客户端就可以开始发送有效的音视频数据了,服务端会一直接收这些数据,直到客户端发出 FCUnpublish 和 deleteStream 命令为止。stream.go 的 TransStart() 方法主要逻辑为接收推流客户端的音视频数据,然后在本地缓存最新的一个数据包,最后将音视频数据发给各个拉流端。其中读取推流客户单音视频数据主要是使用到 rtmp.go 中的 VirReader.Read() 方法,相关代码和注释如下所示。

附媒体头信息解析的部分源码分析。

解析音频头

解析视频头

3.2.2.5 拉流

有了推流客户端的持续推流,拉流客户端就可以通过服务器持续拉取到音视频数据了。RTMP 协议规范的 7.2.2.1 节对拉流过程进行了详细描述。其中,握手、连接和创建流的过程前文已经讲述过了,我们重点关注下 play 命令的过程就行。

同样,我们先用 WireShark 抓包来分析下。客户端通过 640 号包告诉服务器,我想要播放叫 “movie” 的频道。

此处为什么是叫 “movie” 而不是推流时候用的“rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk”,其实这两个指向的是同一个频道,只不过一个用于推流一个用于拉流,我们可以从 livego 的源码来印证这一点。

服务端收到拉流客户端的 play 请求后,会做出响应 "NetStream.Play.Reset","NetStream.Play.Start" ,"NetStream.Play.PublishNotify" 和音视频元数据。这些工作做完后,就可以持续发送音视频数据给拉流客户端了。我们可以通过 livego 源码来加深一下对此过程的理解。

通过 chan 读取推流数据,然后发给拉流客户端。

到此为止整个 RTMP 的主体流程就是这样了,这边不涉及 FLV,HLS 等具体传输协议或者格式转换的源码说明,也就是说 RTMP 服务器怎么收到推流客户端的音视频包也会原封不动地分发给拉流客户端,并没有做额外的处理,不过现在各大云厂商拉流端都支持 http-flv,hls 等传输协议的支持,并且也支持音视频的录制回放点播功能,这块 livego 其实也是支持的。


因为篇幅限制,这边就不再展开介绍,后续有机会,再单独一起学习分享介绍 livego 关于这块逻辑的处理。

四、展望


目前基于 RTMP 协议的直播是国内直播的基准协议,也是各大云厂商都兼容的直播协议,它的多路复用,分包等优秀特性也是各大厂商选择它的一个重要原因。在这个基础之上,也是因为它是应用层协议,腾讯,阿里,声网等大型云厂商,也会对其协议的细节,进行源码的改造,例如实现多路音视频流的混流,单路的录制等功能。


但是 RTMP 也有它自己本身的缺点,时延较高就是 RTMP 一个最大的问题,在实际的生产过程中,即使在比较健康的网络环境中,RTMP 的时延也会有 3~8s,这与各大云厂商给出的 1~3s 理论时延值还是有较大出入的。那么时延会带来哪些问题呢?我们可以想象如下的一些场景:


在线教育,学生提问,老师都讲到下一个知识点了,才看到学生上一个提问。


电商直播,询问宝贝信息,主播“视而不理”。


打赏后迟迟听不到主播的口播感谢。


在别人的呐喊声知道球进了,你看的还是直播吗?


特别是现在直播已经形成产业链的大环境下,很多主播都是将其作为一个职业,很多主播使用在公司同一个网络下进行直播,在公司网络的出口带宽有限的情况下,RTMP 和 FLV 格式的延迟会更加严重,高时延的直播影响了用户和主播的实时互动,也阻碍了一些特殊直播场景的落地,例如带货直播,教育直播等。


以下是使用 RTMP 协议常规的解决方案:

根据实际的网络情况和推流的一些设置,例如关键帧间隔,推流码率等等,时延一般会在 8 秒左右,时延主要来自于 2 个大的方面:


CDN 链路延迟, 这分为两部分,一部分是网络传输延迟。CDN 内部有四段网络传输,假设每段网络传输带来的延迟是 20ms,那这四段延迟便是 100ms;此外,使用 RTMP 帧为传输单位,意味着每个节点都要收满一帧之后才能启动向下游转发的流程;CDN 为了提升并发性能,会有一定的优化发包策略,会增加部分延迟。在网络抖动的场景下,延迟就更加无法控制了,可靠传输协议下,一旦有网络抖动,后续的发送流程都将阻塞,需要等待前序包的重传。


播放端 buffer,这个是延迟的主要来源。公网环境千差万别,推流、CDN 传输、播放接收这几个环节任何一个环节发生网络抖动,都会影响到播放端。为了对抗前边链路的抖动,播放器的常规策略是保留 6s 左右的媒体 buffer。


通过上述说明,我们可以清楚的知道,直播最大的延迟就是在于拉流端(播放端 buffer)的时延,所以如何快速地去消除这个阶段的时延,就是各大云厂商亟待解决的问题,这就是后续各大云厂商推出消除 RTMP 协议时延的新的产品,例如腾讯云的"快"直播,阿里云的超低时延 RTS 直播等等,其实这些直播都引入了 WebRTC 技术,后续我们有机会可以一起学习相关知识。

五、参考资料


1.RTMP 官方文档

2.AMF0

3.AMF3

4.FLV 官方文档

5.FLV 文件格式分析

6.livego 源码

7.手撕rtmp协议专项


作者:vivo 互联网服务器团队-Xiong Langyu

发布于: 2021 年 05 月 17 日阅读数: 567
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
玩转直播系列之RTMP协议和源码解析(2)