流媒体协议之RTMP详解20230513
流媒体协议之RTMP详解
1. 简介
RTMP 协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。随着VR技术的发展,视频直播等领域逐渐活跃起来,RTMP作为业内广泛使用的协议也重新被相关开发者重视起来。
RTMP协议是应用层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。一般是使用端口1935,并且无论传输何种信息包括音视频数据都只有一个socket通道即只有一个物理通道。
RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。
2.客户端与服务端整体交互流程
RTMP客户端与服务端整体交互过程如下图:
rtmp播放流程:
RTMP协议规定,播放一个流媒体有两个前提步骤:第一步,建立一个网络连接(NetConnection);第二步,建立一个网络流(NetStream)。其中,网络连接代表服务器端应用程序和客户端之间基础的连通关系。网络流代表了发送多媒体数据的通道。服务器和客户端之间只能建立一个网络连接,但是基于该连接可以创建很多网络流。
播放一个RTMP协议的流媒体需要经过以下几个步骤:握手,建立连接,建立流,播放。RTMP连接都是以握手作为开始的。建立连接阶段用于建立客户端与服务器之间的“网络连接”;建立流阶段用于建立客户端与服务器之间的“网络流”;播放阶段用于传输视音频数据。
抓包实例:
3.握手链接
rtmp 连接从握手开始。它包含三个固定大小的块。客户端发送的三个块命名为 C0,C1,C2;服务端发送的三个块命名为S0,S1,S2。
3.1.具体流程
握手序列:
客户端通过发送 C0 和 C1 消息来启动握手过程。客户端必须接收到 S1 消息,然后发送 C2 消息。客户端必须接收到S2 消息,然后发送其他数据。
服务端必须接收到 C0 或者 C1 消息,然后发送 S0 和 S1 消息。服务端必须接收到 C2消息,然后发送其他数据。
握手示意图:
3.2.数据格式
3.2.1. complex handshake
3.2.1.1.C0 和 S0 格式
C0 和 S0 包由一个字节组成,下面是 C0/S0 包内的字段:
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ | version | +-+-+-+-+-+-+-+-+ C0 and S0 bits |
version(1 byte):RTMP 的版本,一般为 3。
3.2.1.2.C1 和 S1 格式
C1和S1包含两部分数据:key和digest,分别为如下:
key 和 digest 的顺序是不确定的,也有可能是:(nginx-rtmp中是如下的顺序):
764 bytes key 结构:
- random-data: (offset) bytes
- key-data: 128 bytes
- random-data: (764 - offset - 128 - 4) bytes
- offset: 4 bytes
764 bytes digest 结构:
- offset: 4 bytes
- random-data: (offset) bytes
- digest-data: 32 bytes
- random-data: (764 - 4 - offset - 32) bytes
3.2.1.3.C2 和 S2 格式
3.2.1.4.nginx-rtmp握手抓包实例
在实际工程应用中,一般是客户端将C0、C1块同时发出,服务器在收到C1块之后同时将S0、S1、S2发给客户端。客户端收到S1之后,发送C2给服务端,握手完成
可以看到实际上,客户端县直接发了c0和c1,然后服务端直接发送hanshake:S0 + S1 + S2,最后客户端发送c2。
3.2.2. simple handshake
3.2.2.1.C0 和 S0 格式
C0 和 S0 包由一个字节组成,下面是 C0/S0 包内的字段:
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ | version | +-+-+-+-+-+-+-+-+ C0 and S0 bits |
- version(1 byte):版本。在 C0 包内,这个字段代表客户端请求的 RTMP 版本号。在 S0 包内,这个字段代表服务端选择的 RTMP 版本号。当前使用的版本是 3。版本 0-2 用在早期的产品中,如今已经弃用;版本 4-31 被预留用于后续产品;版本 32-255(为了区分 RTMP 协议和文本协议,文本协议通常是可以打印字符)不允许使用。如果服务器无法识别客户端的版本号,应该回复版本 3,。客户端可以选择降低到版本 3,或者终止握手过程。
3.2.2.2.C1 和 S1 格式
C1 和 S1 包长度为 1536 字节,包含以下字段:
- time(4 bytes):本字段包含一个时间戳,客户端应该使用此字段来标识所有流块的时刻。时间戳取值可以为零或其他任意值。为了同步多个块流,客户端可能希望多个块流使用相同的时间戳。
- zero(4 bytes):本字段必须为零。
- random (1528 bytes):本字段可以包含任意数据。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆,用户区分出其响应C2/S2来自此RTMP连接发起的握手还是其他方发起的握手)。不过没有必要为此使用加密数据或动态数据。
3.2.2.3.C2 和 S2 格式
C2 和 S2 包长度为 1536 字节,作为 C1 和 S1 的回应,包含以下字段:
- time(4 bytes):本字段必须包含对端发送的时间戳。
- time2(4 bytes):本字段必须包含时间戳,取值为接收对端发送过来的握手包的时刻。
- random(1528 bytes):本字段必须包含对端发送过来的随机数据。握手的双方可以使用时间 1 和时间 2 字段来估算网络连接的带宽和/或延迟,但是不一定有用。
4.信息交互
4.1.数据格式
RTMP 协议为了维持稳定连续传递,避免单次传输数据量问题,采用了传输层封包,数据流切片的实现形式。被用来对当前带宽进行划分和复用的最小传输单位,被称为 Chunk 即消息块。通常情况下,一个有效的消息,如果数据量超出当前 Chunk Size 的话,则会被拆分成多个分块来分批传输。通过指定首个Chunk 和后续 Chunk 类型,以及 Chunk Header 其他标志性数据,来使当前被切割的消息,能够在对端得到有效的还原和执行。
消息块格式:
- 块的基本头(1-3字节):这个字段包含块流ID和块类型。块类型决定了编码过的消息头的格式。这个字段是一个变长字段,长度取决于块流ID。
- 消息头(0,3,7,11字节):这个字段包含被发送的消息信息(无论是全部,还是部分)。字段长度由块头中的块类型来决定。
- 扩展时间戳(0,4字节):这个字段是否存在取决于块消息头中编码的时间戳。
- 块数据(可变大小):当前块的有效数据,上限为配置的最大块大小。
4.1.1.Basic Header
包含 chunk stream ID(流通道id)和chunk type(即fmt),chunk stream id 一般被简写为CSID,用来唯一标识一个特定的流通道,同一个Chunk Stream ID必然属于同一个信道,chunk type决定了后面Message Header的格式。
Basic Header的长度可能是 1,2,或 3 个字节,其中 chunk type 的长度是固定的(占2位,单位是bit),Basic Header 的长度取决于 CSID 的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。
RTMP最多可支持65597个流,CSID范围在3-65599 内,CSID 为0, 1, 2 是协议保留的,用于表示特殊信息,具体的(先解析第一个字节):
- CSID=0表示Basic Header块基本头占用 2 个字节,并且CSID范围在64-319 之间(第二个字节+64(2-63使用1字节表示法,2字节表示法就不需要再表示这些));
- CSID=1 表示Basic Header块基本头占用3个字节,并且ID范围在64-65599之间(第三个字节*256 + 第二个字节 + 64(2-63使用1字节表示法,3字节表示法就不需要再表示这些))。
- CSID=2 表示该 chunk 是控制信息和一些命令信息,为低版本协议保留的。
- CSID=3-63 范围内的值表示整个流ID(有效ID)。
4.1.1.1.Basic Header: 1 byte , csid = 2-63
4.1.1.2.Basic Header: 2 byte , csid = 0
CSID占14bit,此时协议将于chunk type所在字节的其他bit都置为0,剩下的一个字节表示CSID - 64,这样共有8个bit来存储 CSID,8 bit 可以表示 [0,255] 个数,因此这种情况下 CSID 在 [64,319],其中 319 = 255 + 64。
4.1.1.3.Basic Header: 3 bytes , csid = 1
CSID占22bit,此时协议将第一个字节的[2,8]bit置1,余下的16个bit表示CSID - 64,这样共有16个bit来存储CSID,16bit可以表示[0,65535]共 65536 个数,因此这种情况下 CSID 在 [64,65599],其中65599=65535+64,
需要注意的是,Basic Header是采用小端存储的方式,越往后的字节数量级越高,因此通过3个字节的每一个bit的值来计算CSID时,应该是: <第三个字节的值> * 256 + <第二个字节的值> + 64.
4.1.2.Message Header
包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,即fmt,共有四种不同的格式。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。下面按字节从多到少的顺序分别介绍这四种格式的 Message Header。
4.1.2.1.Chunk Type(fmt) = 0:11 bytes
type=0时Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream 的开始第一个chunk和头信息中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。
- timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1,当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到 ExtendedTimestamp 字段中,接收端在判断timestamp字段24个位都为1时就会去Extended Timestamp中解析实际的时间戳。
- message length(消息数据长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总长度,而不是chunk本身data的长度。
- message type id(消息的类型id):1个字节,表示实际发送的数据的类型,如8代表音频数据,9代表视频数据。
- message stream id(消息的流id):4个字节,表示该chunk所在的流的ID,和Basic Header的CSID一样,它采用小端存储方式。
4.1.2.2.Chunk Type(fmt) = 1:7 bytes
type为1时占用7个字节,省去了表示message stream id的4个字节,表示此chunk和上一次发的 chunk 所在的流相同,如果在发送端和对端有一个流链接的时候可以尽量采取这种格式。
- timestamp delta:3 bytes,这里和type=0时不同,存储的是和上一个chunk的时间差。类似上面提到的timestamp,当它的值超过3个字节所能表示的最大值时,三个字节都置为1,实际的时间戳差值就会转存到Extended Timestamp字段中,接收端在判断timestamp delta字段24个bit都为1时就会去Extended Timestamp 中解析实际的与上次时间戳的差值。
- 其他字段与上面的解释相同.
4.1.2.3.Chunk Type(fmt) = 2:3 bytes
type 为 2 时占用 3 个字节,相对于 type = 1 格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此 chunk和上一次发送的 chunk 所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示 timestamp delta,使用同type=1。
4.1.2.4.Chunk Type(fmt) = 3: 0 byte
type=3时,为0字节,表示这个chunk的Message Header和上一个是完全相同的。当它跟在type=0的chunk后面时,表示和前一个 chunk 的时间戳都是相同。
什么时候连时间戳都是相同呢?就是一个 Message 拆分成多个 chunk,这个 chunk 和上一个 chunk 同属于一个 Message。而当它跟在 type = 1或 type = 2 的chunk后面时的chunk后面时,表示和前一个 chunk的时间戳的差是相同的。
比如第一个 chunk 的 type = 0,timestamp = 100,第二个 chunk 的 type = 2,timestamp delta = 20,表示时间戳为 100 + 20 = 120,第三个 chunk 的 type = 3,表示 timestamp delta = 20,时间戳为 120 + 20 = 140。
4.1.3.Extended Timestamp
Extended Timestamp(扩展时间戳), 在 chunk 中会有时间戳 timestamp 和时间戳差 timestamp delta,并且它们不会同时存在,只有这两者之一大于3字节能表示的最大数值 0xFFFFFF = 16777215 时,才会用这个字段来表示真正的时间戳,否则这个字段为 0。
扩展时间戳占 4 个字节,能表示的最大数值就是 0xFFFFFFFF = 4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,而不是减去时间戳或者时间戳差的值。
4.1.4.chunk 示例
4.1.4.1.chunk 示例1
本示例展示了一个音频消息流。流中包含有冗余信息。
- 分析第一个 chunk:
1.首先包含第一个 Message 的 chunk 的 chunk type 为 0,因为它前面没有可参考的 chunk,timestamp 为 1000,表示时间戳。
2.type 为 0 的 header 占用 11 个字节,假定 chunk stream id 为 3 < 127,因此 basic header 占用 1 个字节;
3.再加上 data 的 32 字节,因此第一个 chunk 共 44 字节 = 11 + 1 + 32 个字节。
- 分析第二个 chunk:
1.第二个 chunk 和第一个 chunk 的 cs id 和 chunk type id,以及 data 的长度都相同,因此采用 类型 2;
2.可知 timestamp delta = 1020 - 1000 = 20;
3.因此第二个 chunk 占用 36 = 3(message header) + 1(basic header) + 32
- 分析第三个 chunk:
1.第三个 chunk 和第二个 chunk 的 cs id ,chunk type id,以及 data 的长度和时间戳的差值都相同,因此采用 类型 3,省去全部的 Message Header 的信息;
2.因此占用 33 = 1 + 32
- 分析第四个 chunk:
1.第四个 chunk 和第三个 chunk 情况相同,也占用 33 = 1 + 32 个字节。
最后实际发送的chunk如下面表格所示,该表格展示了由此音频流产生的块信息。从第 3 条信息开始,数据传输达到最大优化。每条消息的头部只增加了 1 字节长度。
4.1.4.2.chunk 示例2
本示例展示了一条长消息,由于消息的长度超过了块的最大长度(128字节),此消息在传输时将被分割成若干个块。
由表格知 data 的长度 307 > 128,因此这个 Message 要分割成几个 chunk 发送:
1).第一个 chunk:type = 0,timestamp = 1000,承担 128 个字节的 data,因此共占用 140 = 11 + 1 + 128 个字节。
2).第二个 chunk:同样要发送 128 字节,其他字段(即Message Header 中的几个字段)都与第一个相同,因此采用 类型 3,共 129 = 1 + 128 字节。
3).第三个 chunk:要发送的 data 的长度为 307 - 128 - 128 = 51 字节,还是采用 类型 3,共 1 + 51 = 52 字节。
下面是消息分割后产生的块:
第一个块的头数据显示了消息的长度为 307 字节。
在这两个示例中,类型为 3 的块有两种使用方式。第一种是说明消息的继续。第二种是说明新消息的头信息可以由前面已经存在的消息推导出来。
如果没有看懂没有关系,后续阅读到第3.2节,是具体交互过程可以当作实例来进行验证回顾。
4.1.5. ChunkStreamID与MessageStreamID区别
ChunkStreamID与MessageStreamID是协议分层的概念(各自层次的id即标识或者说各自层次的消息归类)
目前看只有一个socket通道,而且ChunkStreamID只是块流中用于组包消息的,即一条消息拆分chunk后通过ChunkStreamID组装回来,还是标识(区分)逻辑通道的功能,注意不是物理通道。MessageStreamID是对MessageStreamtype消息类型的归类。
message stream id就是指该次媒体流的唯一标识,用于指明属于哪个流,用于区分不同流(NetConnection网络连接流或者是(某个)NetStream网络流)的消息。一个StreamId通常用以完成某些特定的工作. 如使用Id为0的Stream来完成客户端和服务器的连接和控制,用Id为1的Stream来完成Stream的控制和播放等工作.
chunk header中Chunk stream ID 是用来区分消息信道的,因为 RTMP 协议,所有的通信都是通过同一个 TCP 来完成的,因此所有类型的通信信道需要由 Chunk stream ID 来进行区分,从而判断当前收到的消息所属的信道类型,此是由用户定义的,Adobe 建议采用如下的分类:
其中obs推流、ffmpeg推流都有自己的定义方式,与这个略有不同,CSID再解析式可只作为一路流的通道,由Message Type来标识音视频,消息类型定义如下所示:
Message Type
|
Msg Type id
|
MessageStreamID |
作用 |
Set Chunk Size
|
1 |
0 |
通知对端,更新最大可接受的Chunk大小,默认为128 (单位: Bytes) ,最小为1 (单位: Bytes) |
About Message
|
2 |
|
通知另一方,终止处理指定cs_ id信道后续的其他消息 |
Acknowledgement
|
3 |
|
由数据接收方(receiver) 发送,当首次收到有效数据大小等于Window Ack Size消息设置的窗口大小时,发送此消息给数据发送方(sender)以表示链接稳定。 |
User Control Message
|
4 |
0 |
用户控制消息,是-系列用于控制消息流的消息的总称。用来作为控制对端用户操作事件的一种手段 |
Window Ack Size
|
5 |
0 |
由数据发送方(sender) 发送,用来设定接收方(receiver)首次有效数据传输到来之后,用于等待确定传输稳定的窗口大小(单位: Bytes) |
Set Peer Bandwidth
|
6 |
0 |
由数据接收方(receiver) 发送,根据已收到但未确认的消息的数据量,来通知约束发送方(sender) 的输出带宽(单位: Bytes),收到消息需要发送Window AckSize做应答 |
Audio |
8 |
1 |
RTMP音频数据包 |
Video |
9 |
1 |
RTMP视频数据包 |
Data AMF3 |
15 |
|
AMF3编码,音视频MetaData风格|配置 详情包 |
Shared Object AMF3 |
16 |
|
AMF3编码,共享对象消息(携带用户详情) |
Command AMF3 |
17 |
|
AMF3编码,RTMP 命令消息,可能涉及用户数据 |
Data AMFO |
18 |
|
AMFO编码,音视频MetaData风格| 配置详情包 |
Shared Object AMFO |
19 |
|
AMFO编码,共享对象消息(携带用户详情) |
Command AMFO |
20(0x14) |
Connect 0,play 1 |
AMFO编码,RTMP 命令消息,可能涉及用户数据 |
Aggregate Message |
22 |
|
整合消息 |
|
|
|
|
|
|
|
|
4.2.具体流程
4.2.1.Command Message
Command Message(命令消息,Message Type ID = 17 或 20):表示在客户端和服务器间传递的在对端执行某些操作的命令消息,connect 表示连接对端,对端如果同意连接的话就会记录发送端信息并返回连接成功消息,publish 表示开始向对方推流,接收端接收到命令后准备好接收对端发送的流信息。当信息使用 AMF0 编码时,Message Type ID = 20,AMF3 编码时 为 17。
服务器和客户端之间使用 AMF 编码的命令消息交互。 一些命令消息被用来发送操作指令,比如 connect,createStream,public,play,pause。另外一些命令消息被用来通知发送方请求命令的状态,比如 onstatus,result 等。一条命令消息包括命令对称、交互 ID、包含相关参数的命令对象。服务器和客户端通过在创建的流中远程调用的方式,使用命令消息来进行交互。
服务器发送给客户端的命令结构如下:
命令类型:
客户端和服务器通过 AMF 编码的数据交换命令。发送者发送包含命令名称,事务ID,包含相关参数的命令对象的消息。例如,通过连接命令中包含的 APP 参数来告诉服务器连接的对方是哪个客户端。接收方处理命令消息,并使用相同的事务ID应答。应答字符串为 _result 或 _error 或方法名,例如 verifyClient 或 contactExternalServer。事务 ID 标明了应答指向的命令。事务ID相当于 IMAP 协议或其他协议中的标签。命令字符串中的方法名,表明了发送端想要在接收端执行的方法。
下面的类对象被用来发送各种命令(其实还是分层的概念,分为两层):
- NetConnection:服务器和客户端之间进行网络连接的一种高级表示形式。网络连接允许使用以下的命令:连接 connect,调用 call,停止 close,创建流 createStream
- NetStream:代表了发送音频流,视频流,或其他相关数据的频道。当然还有一些像播放,暂停之类的命令,用来控制数据流
4.2.1.1.NetConnection(网络连接命令)
网络连接管理着客户端和服务器之间的双向连接。另外,它也支持异步远程命令调用。
网络连接允许使用以下的命令:
- 连接 connect
- 调用 call
- 停止 close
- 创建流 createStream
4.2.1.1.1.connect
命令执行过程中的消息流如下:
- 客户端发送连接命令给服务器,获得与服务器连接的实例。
- 服务器在接收到连接命令后,发送应答窗口大小的消息给客户端。同时与连接命令中接到的应用建立连接。
- 服务器发送设置流带宽消息给客户端。
- 客户端在接收并处理了设置流带宽的消息后,发送应答窗口大小的消息给服务器。
- 服务器接着发送开始流的用户控制消息给客户端。
- 服务器发送 result 命令消息给客户端,通知连接状态是成功或失败。命令消息中包含了事务ID。消息中还包含了像 FMS版本之类的属性,以及级别,编码,描述,对象编码等信息。
客户端发送连接命令给服务器,来获取一个和服务器通信的实例。客户端发送给服务器的命令结构如下:
下面是连接命令的命令对象里包含的键值对的说明:
音频编码属性的可选值:
- 原始 PCM,ADPCM,MP3,NellyMoser(5,8,11,16,22,44kHz),AAC,Speex。
视频编码属性的可选值:
- Sorenson,V1,On2,V2,H264.
视频函数属性的可选值:
对象编码属性的可选值:
示例
C -> S: 服务器接收客户端 connect 命令消息
S -> C: 服务器响应客户端 connect 成功消息
4.2.2.1.5.createStream
客户端通过发送此消息给服务器来创建一个用于消息交互的逻辑通道。音频,视频,和元数据都是通过 createStream 命令创建的流通道发布出去的。
NetConnection 是默认的交互通道,流 ID 为0. 协议和一部分命令消息,包含 createStream,都是使用默认的交互通道发布的。
从客户端发送给服务器的命令结构如下:
从服务器发送给客户端的命令结构:
示例
C -> S: 服务器接收客户端 createStream 命令消息
C -> S: 服务器响应客户端 createStream 成功消息
4.2.1.2.NetStream(网络流命令)
网络流定义了通过网络连接把音频,视频和数据消息流在客户端和服务器之间进行交换的通道。一个网络连接对象可以有多个网络流,进而支持多个数据流。
客户端可以通过网络流发送到服务器的命令如下:
- 播放play
- 播放2 play2
- 删除流 deleteStream
- 关闭流 closeStream
- 接收音频 receiveAudio
- 接收视频 receiveVideo
- 发布 publish
- 定位 seek
- 暂停 pause
服务器通过发送 onStatus 命令给客户端来通知网络流状态的更新
4.2.1.1.6.play
play:客户端发送此命令来通知服务器开始播放流。多次使用此命令可以创建一个播放列表。如果想要创建一个动态播放列表来在不同的直播或点播流之间切换,可以通过多次调用播放命令,同时将 Reset 字段设置为 false。相反,如果想要立即播放指定的流,先清理掉之前的播放队列,再调用播放命令,同时将 Reset 字段设置为 true。
从客户端发送给服务器的命令结构如下:
play 播放命令执行流程:
命令执行过程中的消息流如下:
- 当客户端接收到服务器返回的 createStream 成功的消息时,开始发送播放命令。
- 服务器接收到播放命令后,发送设置块大小的消息。
- 服务器发送一条用户控制消息,消息内包含了 StreamlsRecorded 事件和流ID。事件类型位于消息的前 2 个字节,流 ID位于消息的最后 4 个字节。
- 服务器发送一条用户控制消息,消息内包含了 StreamBegin 事件,用于通知客户端开始播放流。
- 如果客户端已经成功发送了播放命令,那么服务器发送两条 onStatus 命令给客户端,命令的内容为 NetStream.Play.Start和 NetStream.Play.Reset。服务器只有在客户端发送了设置有重置标签的播放命令后,才能发送 NetStream.Play.Reset命令。如果服务器找不到客户端请求播放的流,那么发送 NetStream.Play.StreamNotFound 命令给客户端。
- 之后,服务器发送音频和视频数据给客户端。
示例:
C -> S: 服务器接收客户端 play 命令消息
S -> C: 服务器响应客户端 play成功消息
其中,StreamlsRecorded后4字节是流ID,表示本次播放使用哪条流,该流ID是前面服务端响应creatStream 命令生成的:
MetaData相关信息,包括分辨率等:
4.2.1.2.2.publish
publish: 发布,客户端发送此消息,用来发布一个有名字的流到服务器。其他客户端可以使用此流名来播放流,接收发布的音频,视频,以及其他数据消息。
客户端发送给服务器的命令结构如下:
服务器接收到此消息后,回复 onStatus 命令来标记发布的开始。
示例1:发布录制的视频
此示例阐述了发布者如果发布视频流到服务器。其他客户端可以订阅并播放此视频流。
示例2:从录制的流发布元数据
本示例展示了发布元数据的消息交换过程。
4.2.1.2.3.seek
seek: 定位,客户端发送此消息来定位多媒体文件或播放列表的偏移(以毫秒为单位)。
客户端发送给服务器的命令结构如下:
当定位完成后,服务器回复 NetStream.Seek.Notify 状态消息给客户端。如果定位失败,将回复 _error 消息。
4.2.1.2.4.pause
pause: 暂停,
客户端发送此消息来通知服务器暂停或开始播放。
客户端发送给服务器的命令结构如下:
当流暂停成功,服务器发送 NetStream.Pause.Notify 状态消息给客户端,如果流未暂停,服务器发送NetStream.Unpause.Notify 状态消息给客户端。如果暂停失败,则发送 _error 消息。
4.2.2.Protocol Control Messages
RTMP 块流使用消息类型 ID 1、2、3、5、6 作为协议控制消息。这些消息包含了必要的 RTMP 块流协议信息。
这些协议控制消息必须使用 0 作为消息流ID(作为已知的控制流ID),同时使用 2 作为块流ID。协议控制消息接收立即生效;解析时,时间戳字段被忽略。
4.2.1.1.2.Window Acknowledgement Size
Window Acknowledgement Size:应答窗口大小,客户端和服务器发送这个消息来通知对方应答窗口的大小。发送方在发送了等于窗口大小的数据之后,等待接收对方的应答消息(在接收到应答之前停止发送数据)。接收方必须发送应答消息,在会话开始时,或从上一次发送应答之后接收到了等于窗口大小的数据。
示例(send ack_size,192.168.110.130是服务端、192.168.110.1是客户端):
4.2.1.1.3.Set Peer Bandwidth
Set Peer Bandwidth:设置流带宽,客户端和服务器发送此消息来说明对方的出口带宽限制。接收方以此来限制自己的出口带宽,即限制未被应答的消息数据大小。接收到此消息的一方,如果窗口大小与上次发送的不一致,应该回复应答窗口大小的消息。
限制类型的取值为下面之一:
- 硬限制(0):应该限制出口带宽为指明的窗口大小。
- 软限制(1):应该限制出口带宽为指明的窗口大小,或已经生效的小一点的窗口大小。
- 动态限制(2):如果上一次为硬限制,此消息被视为硬限制,否则忽略此消息。
示例(send bandwidth,192.168.110.130是服务端、192.168.110.1是客户端):
4.2.1.1.4.Set Chunk Size
Set Chunk Size:设置块大小 (1),协议控制消息(1),设置块大小,被用来通知对方新的最大的块大小。
默认最大的块大小为 128 字节,客户端和服务器可以使用此消息来修改默认的块大小。例如,假设客户端想要发送的音频数据大小为131 字节,而块大小为 128 字节。在这种情况下,客户端可以通知服务器新的块大小为 131 字节,然后就可以使用一个块来发送完整的音频数据了。
最大的块大小至少为 128 字节,块至少携带 1 个字节的内容。通信的每一个方向(例如从客户端到服务器)拥有独立的块大小设置。
- 0:当前比特位必须为零。
- chunk size(31 bits): This field holds the new maximum chunk size, in bytes, which will be used for all of the sender's subsequent chunks until further notice. Valid sizes are 1 to 2147483647(0x7FFFFFFF) inclusive; however, all sizes greater than 16777215(0xFFFFFF) are equivalent since no chunk is larger than onemessage, and no message is larger than 16777215 bytes.
- 块大小(31比特):本字段标识了新的最大块大小,以字节为单位,发送端之后将使用此值作为最大的块大小。本字段的有效值为 1 - 2147483647(0x7FFFFFFF),由于消息的最大长度为 16777215(0xFFFFFF),而一个块最多只能携带一条消息,因此本字段的实际有效值为 1~16777215(0xFFFFFF)。
示例(send chunk size,192.168.110.130是服务端、192.168.110.1是客户端):
4.2.3.User Control Message
User Control Message Events(用户控制消息,Message Type ID = 4):告知对方执行该信息中包含的用户控制事件,比如Stream Begin 事件告知对方流信息开始传输。和前面提到的协议控制信息(Protocol Control Message)不同,用户控制消息是在 RTMP 协议层的,而不是在 RTMP chunk 流协议层,这个很容易弄混。该信息在 chunk 流中发送时,Message Stream ID= 0,Chunk Strean id = 2,Message type id = 4.
用户控制消息应该使用 0 作为消息流 ID,当通过 RTMP 块流发送此消息时,块流 ID 为 2. RTMP 流中的用户控制消息在接收时立即生效,消息中的时间戳被忽略。
客户端或服务器发送此消息用来通知对方用户控制事件。此消息包含事件类型和事件数据。
用户控制消息的前 2 个字节数据用来标识事件类型。事件类型后面是事件数据。事件数据字段是可变的。由于此消息是通过RTMP 块流层发送的,块大小的最大值应该满足在一个块里包含此消息。
用户控制事件支持如下类型:
示例(Stream Begin):
示例(StreamIsRecorded):
4.2.4.Audio Message
Audio Message(音频信息,Message Type ID = 8):音频数据
示例:
4.2.4.1. Audio Data封装格式
所有的音频包是通过AUDIODATA结构封装的,在向RTMP服务器推送音频流或者视频流时,首先要推送一个音频tag(AAC sequence header)和视频tag(AVC sequence header),没有这些信息播放端是无法解码音视频流的,其中音频tag格式如下,即有关Audio Data的封装格式如下图所示:
4.2.4.1.1.AACAUDIODATA格式
其中AACAUDIODATA的结构如下图:
4.2.4.1.1.1.AAC sequence header(AudioSpecificConfiguration)
AAC sequence header也就是包含了AudioSpecificConfig,AudioSpecificConfig包含着一些更加详细音频的信息,AudioSpecificConfig的定义在“ISO-14496-3 Audio”中1.6.2.1 AudioSpecificConfig,在ffmpeg中有对AudioSpecificConfig解析的函数,ff_mpeg4audio_get_config()
AudioSpecificConfig结构的描述非常复杂,这里我做一下简化,事先设定要将要编码的音频格式,其中,选择"AAC-LC"为音频编码,音频采样率为44100,于是AudioSpecificConfig简化为下表:
从上面推论出AAC sequence header内容的前2个字节是0xAF 0x00,我们来看一个示例:
AAC sequence header内容是:AF 00 12 10,解析如下:
4.2.4.1.1.2.AAC raw
发送完AAC sequence header后就是AAC的帧数据,也就是AAC raw数据。
这里的AAC raw数据是ADTS格式的AAC数据,也就是 AAC的纯ES流
RTMP推流需要的是 AAC 的裸数据。所以如果编码出 ADTS 格式的数据,需要去掉7个或者9个字节的 ADTS 头信息。
另一个比较重要的信息就是RTMP包的时间戳问题,其计算公式是:
frame_duration = frame_sample*1000/sample_rate
4.2.4.1.1.3.AAC ADTS格式分析
AAC音频格式有ADIF和ADTS:
ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。
ADTS格式的文件组成如下:
AAC ADTS Header结构如下图所示,1~10字段是固定头部,11~15字段是可变头部,CRC字段不一定有内容:
先看一段AAC ADTS的文件内容:
解析如下:
(7 or 9)个字节的Header内容:11111111 11111001 01010000 10000000 00010111 00111111 11111100
固定头(28bit):
syncword(12bit):11111111 1111,0xFFF,ADTS帧的开始
ID(1bit):1, MPEG Version, 0表示是MPEG-4,1表示MPEG-2
layer(2bit):00,一般都是00
protection_absent(1bit):1,1表示包头没有CRC校验字段,0表示有CRC校验字段
profile(2bit):01, 表示使用哪个级别的AAC
sampling_frequency_index(4bit):0100,表示使用的采样率下标,指明了采样率,这个例子表示使用了44.1kHz
private_bit(1bit):0,private stream, set to 0 when encoding, ignore when decoding
channel_configuration(3bit):010,表示声道数
original_copy(1bit):0,originality, set to 0 when encoding, ignore when decoding,
home(1bit):0,set to 0 when encoding, ignore when decoding
可变头:
copyright_identification_bit(1bit):0,copyrighted stream, set to 0 when encoding, ignore when decoding
copyright_identification_start(1bit):0,copyright start, set to 0 when encoding, ignore when decoding
aac_frame_length(13bit):00 00010111 001(185),AAC帧的长度,该值包含头部的长度,FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
adts_buffer_fullness(11bit):11111 111111(0x7FF),说明码率可变的码流
no_of_raw_data_blocks_in_frame(2bit):00,Number of AAC frames (RDBs) in ADTS frame minus 1, for maximum compatibility always use 1 AAC frame per ADTS frame
CRC(16bit):CRC的校验字段,该示例没有校验字段。
4.2.4.2. Audio Data报文解析示例
1).第一个Audio Data
AF 00 12 10 //aac序列头,前面已有具体解释
2).第二个Audio Data
AF 01 //后面是aac音频数据(aac raw)
4.2.5.Video Message
Video Message(视频信息,Message Type ID = 9):视频数据
示例:
4.2.5.1. Video Data封装格式
有关Video Data的封装格式如下图所示(video_file_format_spec_v10 flv/f4v规范):
1).帧类型(frametype):占高4位,定义视频的帧类型
2).编码类型(CodecId):占低四位,定义了视频的编码格式,H264为7,videodata采用AVCVIDEOPACKET格式
3).视频数据(videoData):根据编码类型选择对应的视频数据封装格式,这里介绍H264,选择AVCVIDEOPACKET格式
4.2.5.1.1.AVCVIDEOPACKET格式
AVCVIDEOPACKET格式如下图所示:
1).AVC包类型(AVCPacketType):0表示为AVCC的序列头,这里视频打包格式不是我们常见的Annex B(H.264码流分Annex-B和AVCC两种格式。H.265码流是Annex-B和HVCC格式。),而是AVCC方式,H264采用AVCC封包时,解码需要AVCC序列头,客户端解码必须接收到此序列头包才会解码;1表示H264的nalu,此包为H264数据包,与Annex B相比去除了nalu起始码00000001,增加了四个字节的nalu长度
2).Comosition time offset:组合帧时间便宜,一般没啥用,直接赋值为0
3).视频数据(Data):根据AVC包类型,封装相应的数据,AVCPacketType=0时按照AVCC序列头格式封装,为1时按照nalu封装。RTMP发送视频数据时,客户端接收到AVCC 序列头之后,才能初始化解码器,进行后续nalu帧的解码操作,这里为了防止第一包数据丢失,一般服务器会在每一个IDR帧之前,都发送一包AVC sequence header,保证客户端能够在极端
情况下,具备持续解码播放的能力,这里从RTMP发送AVC sequence header、AVC Nalu来详细介绍封装格式。
4.2.5.1.2. NALU分割格式
H264的主要目标是实现高的视频压缩比和提供良好的网络亲和性(可适用于各种网络传输),因此在功能层面上划分为视频编码层VCL和网络提取层NAL两层
其中预测(帧内预测和帧间预测)、DCT、量化、编码和切片等步骤都属于VCL,VCL最终会被包装为NAL
NAL由一系列NAL Unit组成
不过接下来并不对NALU进行展开,而是关注另一个问题,即:在H264码流中是如何将NALU进行分割的
要搞清楚这点,就需要了解目前H264主流的码流组织方式AnnexB和AVCC两种格式,其中Android的硬解码MediaCodec只支持AnnexB格式的数据,而Apple的VideoToolBox只支持AVCC
区别有两点:一个是参数集(SPS, PPS)组织格式;一个是分隔。
- Annex-B:使用start code分隔NAL(start code为三字节或四字节,0x000001或0x00000001,一般是四字节);SPS和PPS按流的方式写在头部。
- AVCC:使用NALU长度(固定字节,通常为4字节)分隔NAL;在头部包含extradata(或sequence header)的结构体。(extradata包含分隔的字节数、SPS和PPS)
4.2.5.1.2.1.Annex-B格式
Annex-B格式 也叫MPEG-2 transport stream format格式(ts格式), ElementaryStream格式。用于TS流中,以及使用TS作为切片的HLS格式中。
Annex-B 附录B, 指ITU-T的 Recommendation(h.264和h.265)在附录B中规定码流格式。
AnnexB格式每个NALU都包含起始码,且通常会周期性的在关键帧之前重复SPS和PPS所以解码器可以从视频流随机点开始进行解码,实时的流格式,用于实时播放,
它的原理是通过在NALU前面添加一个叫Start Code(起始码)的东西,起始码的内容为三字节的0 0 1或者4字节的0 0 0 1 (其中起始码在NALU为SPS、PPS或NALU为AU的第一个NALU时使用4字节,其他情况使用3字节)当我们读取一个 H264码流的时候,一旦遇到起始码,就认为一个新的 NALU 开始了
不过在NALU前加入起始码会引入一个新的问题,因为原始码流中是可能出现和起始码一样的数据,这样就会导致错误的NALU分割。为了防止这种情况发生,AnnexB 引入了防竞争字节(Emulation Prevention Bytes)的概念
具体操作为:编码器编完一个NALU后,检查内部是否出现如下左侧的字节序列,如果存在,则在最后一个字节前插入一个新的字节0x03
0x00 => 0x00
0x00 => 0x00
0x00 => 0x00
0x00 => 0x00
解码器在NALU内部检测到防竞争字节后将0x03丢弃来恢复原始数据
4.2.5.1.2.2.AVCC格式
AVCC格式也叫AVC1格式,MPEG-4格式,常用于mp4/flv等封装中。
这种格式通常被用于可以被随机访问的多媒体数据,如存储在硬盘的文件。MP4、MKV通常用AVCC格式来存储。
它的原理是在NALU 前面添加固定字节(可能是1字节、2字节或4字节,其中4字节较常见),这几个字节组成一个整数(大端字节序)表示整个 NALU 的长度,在读取的时候,先把这个整数读出来(例如ffmpeg从extradata获取),拿到这个 NALU 的长度,再按照长度读取整个 NALU。
1). AVC 序列头(AVC sequence header or extradata)
按位读取
8 version ( always 0x01 )
8 avc profile ( sps[0][1] )
8 avc compatibility ( sps[0][2] )
8 avc level ( sps[0][3] )
6 reserved ( all bits on )
2 NALULengthSizeMinusOne // 每个NALU数据长度所占byte-1
3 reserved ( all bits on )
5 number of SPS NALUs (usually 1)
repeated once per SPS:
16 SPS size
variable SPS NALU data
8 number of PPS NALUs (usually 1)
repeated once per PPS
16 PPS size
variable PPS NALU data
读取 NALULengthSizeMinusOne的值然后加 1 ,我们就得出了后续每个 NALU 前面表示NAL size(也就是上图紫色块length)的字节数,一般NALULengthSizeMinusOne 是 3,那么每个 NALU length就是 4 个字节,然后把这四个字节转成整数,就是这个 NALU 的长度
————————————————
RTMP发送AVC sequence header,其Video data封包方式(设包为buf数组),第一个字节为buf[0]=0x17=0B00010111,其frametype为关键帧,所以高四位为1,编码类型为H264,所以低四位为7,其视频数据采用AVCVIDEOPACKET格式,AVCPacketType为AVC序列头,所以buf[1]=0,buf[2,3,4]={0,0,0},此时其Data的格式为AVCDecoderConfigurationRecord(就是AVC sequence header),AVC sequence header格式如下
版本号固定为1,因此buf[5]=1,后面三个字节表示编码规格、编码兼容性和编码等级,从SPS中即可获取,所以buf[6]=SPS[1],buf[7=SPS[2],buf[8]=SPS[3],lengthSizeMinusOne表示用多少字节来表示nalu size,一般默认为3,其值+1,表示nalu要使用4个字节来表示长度,buf[9]= 0xFC|0x3=0xFF,下一个字节表示SPS个数,H264一般SPS只有一个,因此buf[10]=E0|01=E1,接下来就是SPS size和SPS data,SPS size站2个字节,buf[11,12,13,14]=SPS size(大端模式),后面直接为SPS的数据,这里注意要去掉起始码之后的数据,假设去掉起始码之后SPS为24,则buf[11-12]=0x0018,buf[13-26]= SPS data,下一个字节为PPS个数,一般为1,buf[27]=1,之后为PPS Size和PPS Data,这里假设PPS Size=4,则buf[28-29]=0x04,buf[30-33]=PPS Data,至此RTMP传输的起始帧AVC sequence header组装完毕,此帧可每次随IDR帧重复发送
2). AVC NALU
AVC NALU 等价于Length(NALU 的长度)+NALU
RTMP的视频帧发送很简单,如果为I帧,buf[0]=0x17,表示关键帧,H264编码;视频帧AVCPacketType类型为AVC Nalu,所以buf[1]=1,buf[2,3,4]={0,0,0},此时其Data的格式为AVCC Nalu封装方式,即是4字节nalu大小+nalu数据(去掉起始码),所以buf[5-8]=nalu size,buf[9-n]= nalu data,至此I帧的RTMP 负载就组合完了;P帧与I帧就第一个字节不同,P帧不是关键帧,因此frametype=2,buf[0]=0x27
3).参考
1.【ISO-14496-10】
http://www.staroceans.org/e-book/ISO-14496-10.pdf
2.【H264编码格式整理】
https://zhuanlan.zhihu.com/p/71928833
3.【逐字节详解H.264 AVCC header】
https://www.jianshu.com/p/4f95617f30d0
4.2.5.2. Video Data报文解析示例
1).关键帧
17 :关键帧,h264
00 :AVCPacketType ,0表示为AVCC的序列头,这里视频打包格式不是我们常见的Annex B,而是AVCC方式
00 00 00 : Comosition time offset:一般没啥用,直接赋值为0
01 :AVC sequence header or extradata版本号 1
64 :avc profile ( sps[0][1] )
00:avc compatibility ( sps[0][2] )
0d:avc level ( sps[0][3] )
ff:6 reserved ( all bits on ) 2 NALULengthSizeMinusOne // 每个NALU数据长度所占byte-1,nalu size=4
e1 :3 reserved ( all bits on ) 5 number of SPS NALUs (usually 1) , SPS 个数 =1
00 1d :SPS size = 29
67 64 00 0d ac d9 41 e1 bb ff 00 10 00 0f 10 00 00 03 00 10 00 00 03 03 20 f1 42 99 60 :SPS
01 :number of PPS NALUs (usually 1),PPS 个数 =1
00 06 : PPS size=6
68 eb e3 cb 22 c0 : PPS,68是nalutype
2).非关键帧
07 00 00 00 00 36 16 09 01 00 00 00 //rtmp header
17 01 00 00 50 //01 nalu
00 00 02 ae //长度
06 05 ff ff aa//06 nalu type sei
3).另一个例子:
5.参考文献
参考文献:
[1] RTMP 官方规范(https://www.adobe.com/devnet/rtmp.html)