nginx-rtmp协议解读

客官°小女子只卖身不卖艺 2022-10-06 15:43 487阅读 0赞

前言

1.RTMP(实时消息传输协议)是Adobe 公司开发的一个基于TCP的应用层协议。
2.RTMP协议中基本的数据单元称为消息(Message)。
3.当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块(Chunk)。
在这里插入图片描述

一、握手协议

要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点:

  • 客户端要等收到S1之后才能发送C2
  • 客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)
  • 服务端要等到收到C0之后发送S1
  • 服务端必须等到收到C1之后才能发送S2
  • 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)
    如果每次发送一个握手chunk的话握手顺序会是这样:

在这里插入图片描述

在实际工程应用中,一般是客户端先将C0, C1块同时发出,服务器在收到C1 之后同时将S0, S1, S2发给客户端。之后客户端向服务器端发送C2块,简单握手完成。

  1. clientServer
  2. |---C0+C1—->|
  3. |<--S0+S1+S2 |
  4. |---C2---->

下面是以此抓包截图:
在这里插入图片描述
握手数据格式:

在 Flash10.1 之后,Adobe 对 RTMP 握手进行了一轮修改,握手的步骤和上文记述的没有不同,而是对握手的数据进行了修改,采用了加密数据。因此,现在存在两种握手数据,一般通过C1[4:8]是否为0来区分。我们将使用0值称为简单握手(simple handshake),将非0值的称为复杂握手(complex handshake)。

握手状态

Uninitialized
(未初始化):协议的版本在这个状态中被发送。客户端在数据包C0中将协议版本发出,如果服务端支持这个版本,将在回应中发送S0和S1,并进入Version
Sent状态。如果不支持,服务端会终止连接。 Version Sent
(版本已发送):客户端和服务端分别等待接收S1和C1。接收完成后,将会进入Ack Sent状态。 Ack Sent
(确认已发送):客户端和服务端分别等待接收S2和C2。接收完成后,进入Handshake Done状态。 Handshake Done
(握手完成):客户端和服务端可以开始交换消息了。
参考:https://www.jianshu.com/p/dc4fc2b58362

1.1 complex handshake

Rtmp握手
•客户端发送C0+C1一共是1537个bytes.
•服务端发送S0+S1+S2.
•客户端发送C2
•到此握手建立完成,双方收到C2和S2以后就可以发送命令了,C0,S0都占一个字节保存版本号,C1,S1占1536个字节一次是时间戳4bytes,0四个bytes,随机数填满。C2和S2占1536bytes,具体和C1差不多。

1.1.1 C0 和 S0 格式

C0 和 S0 包由一个字节组成,下面是 C0/S0 包内的字段:

  1. 0 1 2 3 4 5 6 7
  2. +-+-+-+-+-+-+-+-+
  3. | version |
  4. +-+-+-+-+-+-+-+-+
  5. C0 and S0 bits
  6. version1 byte):RTMP 的版本,一般为 3

version(1 byte):版本号,表示客户端请求的 RTMP 版本号或服务端支持的 RTMP 版本号。当前使用的版本是3。

1.1.2 C1 和 S1 格式

在这里插入图片描述
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

1.1.3 C2 和 S2 格式

在这里插入图片描述

1.2 simple handshake

1.2.1 C0 和 S0 格式

C0 和 S0 包由一个字节组成,下面是 C0/S0 包内的字段:
在这里插入图片描述

version(1 byte):
版本 在 C0 包内,这个字段代表客户端请求的 RTMP 版本号。 在 S0 包内,这个字段代表服务端选择的
RTMP 版本号。
当前使用的版本是 3。版本 0-2 用在早期的产品中,如今已经弃用; 版本 4-31 被预留用于后续产品; 版本
32-255 (为了区分 RTMP 协议和文本协议,文本协议通常是可以打印字符)不允许使用。 如果服务器无法识别客户端的版本号,应该回复版本3。
客户端可以选择降低到版本 3,或者终止握手过程。

1.2.2 C1 和 S1 格式

C1 和 S1 包长度为 1536 字节,包含以下字段

在这里插入图片描述
time(4 bytes):本字段包含一个时间戳,客户端应该使用此字段来标识所有流块的时刻。时间戳取值可以为零或其他任意值。为了同步多个块流,客户端可能希望多个块流使用相同的时间戳。
zero(4 bytes):本字段必须为零。
random (1528 bytes):本字段可以包含任意数据。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没有必要为此使用加密数据或动态数据。

1.2.3 C2 和 S2 格式

C2 和 S2 包长度为 1536 字节,作为 C1 和 S1 的回应,包含以下字段:

在这里插入图片描述
time(4 bytes):本字段必须包含对端发送的时间戳。
time2(4 bytes):本字段必须包含时间戳,取值为接收对端发送过来的握手包的时刻。
random(1528 bytes):本字段必须包含对端发送过来的随机数据。握手的双方可以使用时间 1 和时间 2 字段来估算网络连接的带宽和/或延迟,但是不一定有用。

1.3 RTMP Chunk Stream

Chunk Stream是对传输RTMP Chunk的流的逻辑上的抽象,客户端和服务器之间有关RTMP的信息都在这个流上通信。这个流上的操作也是我们关注RTMP协议的重点。
在RTMP协议中信令和媒体数据都称之为Message,在网络中传输这些Message,为了区分它们肯定是要加一个Message head的,所以RTMP协议也有一个Message head,还有一个问题因为RTMP协议是基于TCP的,由于TCP的包长度是有限制的(一般来说不超过1500个字节),而RTMP的Message长度是有可能很大的,像一个视频帧的包可能会有几十甚至几千K,这个问题就必然有一个分片的问题,在RTMP协议中对应的说法就是chunk,每一个Message + Message head都是由一个和多个chunk组成的。

RTMP包头 (Message(消息头))
在这里插入图片描述

Basic Header + Message Header =1+11 = 12字节
其实就是把chunk包组合一下重新构成一个新的RTMP包。

RTMP协议封包 由一个Message head包头和一个Message 包体组成。
包头可以是4种长度的任意一种:12, 8, 4, 1byte(s).
完整的RTMP包头应该是12bytes,包含了时间戳,Head_Type,AMFSize,AMFType,StreamID信息,
8字节的包头只纪录了时间戳,Head_Type,AMFSize,AMFType,
4个字节的包头记录了时间戳,Head_Type。
1个字节的包头只记录了Head_Type
包体最大长度默认为128字节,通过chunkSize可改变包体最大长度,通常当一段AFM数据超过128字节后,超过128的部分就放到了其他的RTMP封包中,包头为一个字节。

完整的RTMP包头有12字节,由下面5个部分组成:
在这里插入图片描述

a) Head_Type - 包头类型

Head_Type占用RTMP包的第一个字节,这个字节里面记录了包的类型和包的ChannelID。Head_Type字节的前两个Bit决定了包头的长度.它可以用掩码0xC0进行”与”计算:

Head_Type的前两个Bit和长度对应关系:
在这里插入图片描述
Head_Type的后面6个Bit和StreamID决定了ChannelID。 StreamID和ChannelID对应关系:StreamID=(ChannelID-4)/5+1 。
参考red5。在这里插入图片描述
例如: 在rtmp包的数据中里面,发现被插入了一个0xC2,这个就是一字节的包头,并且channelID=2.

b) TiMMER - 时间戳

时间戳占用RTMP包头的第2、3、4三个字节。
RTMP时间戳可分为绝对时间戳和相对时间戳,纪录的是音视频的时间信息。
相对时间戳指的是二个RTMP包之间的时间间隔,单位毫秒。
绝对时间戳指的是当前封包发送的时刻,单位也是毫秒。
对于音视频的播放,时间戳非常关键,因为音视频的播放同步是由时间戳来控制的,如果你的视频出现卡顿,音视频不同步,延时越来越大,很可能就是你的时间戳不准导致的。
fms对于同一个流,发布(publish)的时间戳和播放(play)的时间戳是有区别的
publish时间戳,采用相对时间戳,时间戳值等于当前媒体包的绝对时间戳与上个媒体包的绝对时间戳之间的差距,也就是说音视频时间戳在一个时间轴上面.单位毫秒。
play时间戳,也是相对时间戳,时间戳值等于当前媒体包的绝对时间戳与上个同类型媒体包的绝对时间戳之间的差距,
注意这里跟上面不同的是强调“同类型的媒体包”。也就是说音视频时间戳分别采用单独的时间轴,单位毫秒。
flv格式文件时间戳,绝对时间戳,时间戳长度3个字节。超过0xFFFFFF后时间戳值等于TimeStamp & 0xFFFFFF。
flv格式文件影片总时间长度保存在onMetaData的duration属性里面,长度为8个字节,是一个double类型。

c)AMFSize - 数据大小

AMFSize占3个字节,这个长度是AMF长度,可超过RTMP包的最大长度128字节。如果超过了128字节,那么由多个后续RTMP封包组合Chunk,每个后续RTMP封包(Chunk)的头只占一个字节。一般就是以0xC?开头。1个字节的包头表示这个包的时间戳、数据大小、数据类型、流ID都和上一个相同ChannelID的RTMP包完全一样。

d)AMFType - 数据类型

AMFType是RTMP包里面的数据的类型,占用1个字节。例如音频包的类型为8,视频包的类型为9。下面列出的是常用的数据类型:在这里插入图片描述

e)StreamID - 流 ID

占用RTMP包头的最后4个字节,是一个big-endian的int型数据。
我们x86计算机内存中数据存放都是小尾数模式:little-endian,而网络数据流一般都是大尾数模式:big-endian。
StreamID是音视频流的唯一ID,一路流如果既有音频包又有视频包,那么这路流音频包的StreamID和他视频包的StreamID相同,但ChannelID不同。
ChannelID 和StreamID之间的计算公式:StreamID=(ChannelID-4)/5+1 参考red5。
如果这个封包既不是音频包,也不是视频包,那么他的StreamID=0.
例如当音视频包ChannelID为2、3、4时StreamID都为1 当音视频包ChannelID为9的时候StreamID为2。

f ) RTMP 封包分析

更加确切的说这是一个chunk包,关于message包和chunk包区别,可以这样说,RTMP包(message包)也就是一个包两个名字,这个包在实际传输中并不存在,在实际环境中是通过chunk包来传输的。最后接收者通过chunk包组合成一个RTMP包(你的,明白?)

这个数据结构在真是的传输环境中是通过chunk包来实现的。

例如有一个RTMP封包的数据 (多个chunk包组合而成的包)
0300 00 00 00 01 02 1400 00 00 00 0200 07 63 6F 6E 6E 65 63 74 003F F0 00 00 00 00 00 00 08 ,,,
数据依次解析的含义
03表示12字节头,channelid=3
000000表示时间戳 Timer=0
000102表示AMFSize=18
14表示AMFType=Invoke 方法调用
00 00 00 00 表示StreamID = 0
到此,12字节RTMP头结束.

下面的是AMF数据分析,具体的AMF0数据格式
AMF数据
02表示String
0007表示String长度7
63 6F 6E 6E 65 63 74 是String的Ascall值”connect”
00表示Double
3F F0 00 00 00 00 00 00 表示double的0.0
08表示Map数据开始

g ) AMF数据

Rtmp包默认的最大长度为128字节,(或通过chunksize改变rtmp包最大长度),当AMF数据超过128Byte的时候就可能有多个rtmp(chunk)包组成,如果需要解码的rtmp包太长则被rtmp协议分割成多个rtmp包.那么解码的时候需要先将包含rtmp包合并, 再把合并的数据解码,解码后可得到amf格式的数据,将这些AMF数据取出来就可以对AMF数据解码了.
RTMP封包包括包头和AMF数据2部分,AMF数据里面可以是命令也可以是音视频数据。
组成服务器和Flash客户端之间的所有数据都是用AMF格式的数据在传送,例如connect() publish()等命令。
AMF数据由2部分组成:
ObjType 和 ObjValue。
ObjType的大小为一个字节。
ObjValue的大小不固定,和ObjType相关。
常用的ObjType类型和对应的ObjValue大小整理如下,详细的ObjType的数据在本文的最下面列出:
在这里插入图片描述
ObjValue不一定是一个固定的大小,他可以包含另外一个AMF数据,这另外一个AMF数据里面又有ObjType 加上 ObjValue,也就是AMF数据的嵌套关系
AMF0数据的嵌套关系如下:
Object={ObjType + ObjValue}
CORE_BOOLEAN={Value(1 Byte)}
CORE_NUMBER={Value(8 Byte)}
CORE_String={StringLen(2 Byte) + StringValue(StringLen Byte)}
CORE_DATE={value(10 Byte)}
CORE_Array={ArrayLen(4 Byte) + Object}
CORE_Map={MapNum(4 Byte) + CORE_Object}
CORE_Object={CORE_String + Object}

看起来有些复杂,所以我这里图文并茂来详解,例如完成握手后,Flash向FMS发送的第一个RTMP数据,内容如下:
在这里插入图片描述
蓝色的表示包头,红色的表示ObjType,绿色的表示数据。上面一段数据由2个RTMP包组成,2个RTMP包头分别用蓝色表示,第一个蓝色的是12字节的包头,后面一个蓝色的C3是一个字节的包头,绿色部分是数据,红色的是AMF数据类型,整个RTMP解码过程如下
[2008-06-18 16:59:20] DecodeInvoke:
[2008-06-18 16:59:20] InvokeName:String:connect
[2008-06-18 16:59:20] InvokeID:Double:0
[2008-06-18 16:59:20] Map:MapNum:0
[2008-06-18 16:59:20] Params:{
[2008-06-18 16:59:20] Key:String:objectEncoding
[2008-06-18 16:59:20] Value:Double:0
[2008-06-18 16:59:20] Key:String:app
[2008-06-18 16:59:20] Value:String:mediaserver
[2008-06-18 16:59:20] Key:String:fpda
[2008-06-18 16:59:20] Value:Bool:0
[2008-06-18 16:59:20] Key:String:tcUrl
[2008-06-18 16:59:20] Value:String:rtmp://127.0.0.1/mediaserver
[2008-06-18 16:59:20] Key:String:audioCodecs
[2008-06-18 16:59:20] Value:Double:615
[2008-06-18 16:59:20] Key:String:videoCodecs
[2008-06-18 16:59:20] Value:Double:76
[2008-06-18 16:59:20] }End Params
[2008-06-18 16:59:20] InvokeParams:String:PUBLISHER
[2008-06-18 16:59:20] InvokeParams:String:streamRecode

ps:

FMS3中为了实现H.264数据的直播而增加了一个数据类型,这个类型的值为0x16,这个类型

关于rtmp封包中数据类型为0x16的封包 。
使用rtmp协议从FMS3中拉音视频数据的时候,会收到AMFType=0x16的封包,这种包在FMS2中从没有出现过.
rtmp包头的第8个字节就是AMFType,也就是数据类型。例如AMFType=0x08表示音频包,AMFType=0x04表示Ping包等等。FMS3中为了实现H.264数据的直播而增加了一个数据类型,这个类型的值为0x16。AMFType=0x16的包中既包含了音频帧也包含了视频帧。其中音频帧和视频帧是一种新的格式存放的,类似FLV文件存储格式,每个音视频包作为一个Tag,许多的Tag组成了这个AMFType=0x16的数据类型,Tag的格式如下:在这里插入图片描述

Chunking(Message分块)

RTMP在收发数据的时候并不是以Message包为单位的,而是把Message包拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。

为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。(具体的压缩方式会在后面介绍)

Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引入过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 块的基本头(Basic Header)(1-3字节):这个字段包含块流ID和块类型。块类型决定了编码过的消息头的格式。这个字段是一个变 长字段,长度取决于块流ID。
  • 消息头(Message Header)(0,3,7,11字节):这个字段包含被发送的消息信息(无论是全部,还是部分)。字段长度由块头中的 块类型来决定。
  • 扩展时间戳(Extenderd Timestamp)(0,4字节):这个字段是否存在取决于块消息头中编码的时间戳。 块数据(可变大小):当前块的有效数据,上限为配置的最大块大小

关于Message Header这里为什么是11字节,而前面说的是12字节,这是因为前面将Basic Header也算到里面去了,一般来说我们常见的组合是 Basic Header + Message Header =1+11 = 12字节

1.3.1 Basic Header

包含 chunk stream ID(流通道id)和chunk type(即fmt),chunk stream id 一般被简写为CSID,用来唯一标识一个特定的流通道,chunk type决定了后面Message Header的格式。
Basic Header的长度可能是 1,2,或 3 个字节,其中 chunk type 的长度是固定的(占2位,单位是bit),Basic Header 的长度取决于 CSID 的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。

RTMP协议支持用户自定义 [3,65599] 之间的 CSID,0, 1, 2 由协议保留表示特殊信息。0 代表 Basic Header 总共要占用 2 个字节,CSID 在 [64,319] 之间; 1 代表占用 3 个字节,CSID 在 [64,65599] 之间; 2 代表该 chunk 是控制信息和一些命令信息

1.3.1.1 Basic Header:1 byte

在这里插入图片描述

1.3.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。
在这里插入图片描述

1.3.1.3 Basic Header: 3 byte , 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,需要注意的是, BasicHeader是采用小端存储的方式,越往后的字节数量级越高,因此通过3个字节的每一个bit的值来计算CSID时, 应该是: <第三个字节的值> * 256 + <第二个字节的值> + 64.

1.3.2 Message Header

包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于BasicHeader的chunk type,即fmt,共有四种不同的格式。
其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意 义的数据。下面按字节从多到少的顺序分别介绍这四种格式的 Message Header。

Message Header 四种消息头格式。

1.3.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一样,它采用小端存储方式。

1.3.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
中解析实际的与上次时间戳的差值。 其他字段与上面的解释相同.

1.3.2.3 Chunk Type(fmt) = 2:3 bytes

type 为 2 时占用 3 个字节,相对于 type = 1 格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk 和上一次发送的 chunk 所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示 timestamp delta,使用同type=1。

1.3.2.4 Chunk Type(fmt) = 3:0 bytes

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,

在这里插入图片描述

1.3.3 Extended Timestamp(扩展时间戳)

在 chunk 中会有时间戳 timestamp 和时间戳差 timestamp delta,并且它们不会同时存在,只有这两者之一大于3字节能表示的 最大数值 0xFFFFFF = 16777215时,才会用这个字段来表示真正的时间戳,否则这个字段为 0。扩展时间戳占 4 个字节, 能表示的最大数值就是 0xFFFFFFFF = 4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1, 而不是减去时间戳或者时间戳差的值。

2 协议控制消息

RTMP 块流使用消息类型 ID 1、2、3、5、6 作为控制消息。这些消息包含了必要的 RTMP 块流协议信息。

这些协议控制消息必须使用 0 作为消息流ID(作为已知的控制流ID),同时使用 2 作为块流ID。协议控制消息接收立即生效;
解析时,时间戳字段被忽略。

2.1 设置块大小 (1)

协议控制消息(1),设置块大小,被用来通知对方新的最大的块大小。
4个字节
在这里插入图片描述

默认最大的块大小为 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)。
在这里插入图片描述

2.2 中断消息 (2)

协议控制消息(2),中断消息,用来通知通信的对方,如果正在等待一条消息的部分块(已经接收了一部分),那么可以丢弃之前已经接收到的块。通信的一方将接收到块流ID作为当前协议消息的有效数据。应用程序可以发送此消息来通知对方,当前正在传输的消息没有必要再处理了。

在这里插入图片描述

2.3 应答(3)

客户端和服务器在接收到与接收窗口大小相等的数据后,必须发送应答消息给对方。窗口大小的定义为发送方在接收到接收方的任何应答前,可以发送的最大数据量。本消息包含了序列号,序列号为截至目前接收到的数据总和,以字节为单位。

在这里插入图片描述

2.4 应答窗口大小(5)

客户端和服务器发送这个消息来通知对方应答窗口的大小。发送方在发送了等于窗口大小的数据之后,等待接收对方的应答消息(在接收到应答之前停止发送数据)。接收方必须发送应答消息,在会话开始时,或从上一次发送应答之后接收到了等于窗口大小的数据。

在这里插入图片描述

2.5 3.5 设置流带宽(6)

客户端和服务器发送此消息来说明对方的出口带宽限制。接收方以此来限制自己的出口带宽,即限制未被应答的消息数据大
小。接收到此消息的一方,如果窗口大小与上次发送的不一致,应该回复应答窗口大小的消息。
在这里插入图片描述

3 Command Message (17 或 20)

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:服务器和客户端之间进行网络连接的一种高级表示形式。
NetStream:代表了发送音频流,视频流,或其他相关数据的频道。当然还有一些像播放,暂停之类的命令,据流。

3.1 网络连接命令

网络连接管理着客户端和服务器之间的双向连接。另外,它也支持异步远程命令调用。
网络连接允许使用以下的命令:

连接 connect
调用 call
停止 close
创建流 createStream
建立网络连接

3.1 网络连接命令(NetConnection)

3.1 .1 connect: 连接

命令执行过程中的消息流如下:

客户端发送连接命令给服务器,获得与服务器连接的实例。
服务器在接收到连接命令后,发送应答窗口大小的消息给客户端。同时与连接命令中接到的应用建立连接。
服务器发送设置流带宽消息给客户端。
客户端在接收并处理了设置流带宽的消息后,发送应答窗口大小的消息给服务器。
服务器接着发送开始流的用户控制消息给客户端。
服务器发送 result 命令消息给客户端,通知连接状态是成功或失败。命令消息中包含了事务ID。消息中还包含了像 FMS版本之类的属性,以及级别,编码,描述,对象编码等信息。
在这里插入图片描述
在这里插入图片描述
客户端发送连接命令给服务器,来获取一个和服务器通信的实例。客户端发送给服务器的命令结构如下:
在这里插入图片描述
下面是连接命令的命令对象Command Object里包含的键值对的说明:
在这里插入图片描述
音频编码属性的可选值:
原始 PCM,ADPCM,MP3,NellyMoser(5,8,11,16,22,44kHz),AAC,Speex。

在这里插入图片描述

视频编码属性的可选值:

Sorenson,V1,On2,V2,H264.
在这里插入图片描述
视频函数属性的可选值:
在这里插入图片描述

对象编码属性的可选值:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.1 .2 call: 调用

网络连接对象中包含的 call 方法,会在接收端执行远程过程调用(RPC)。被调用的 RPC 方法名作为 call 方法的参数传输。

从发送端到接收端的命令结构如下

在这里插入图片描述
应答的命令结构如下
在这里插入图片描述

3.1.3 createStream: 创建流

在这里插入图片描述

客户端通过发送此消息给服务器来创建一个用于消息交互的逻辑通道。音频,视频,和元数据都是通过 createStream 命令创建的流通道发布出去的。

NetConnection 是默认的交互通道,流 ID 为0. 协议和一部分命令消息,包含 createStream,都是使用默认的交互通道发布的。

从客户端发送给服务器的命令结构如下:

在这里插入图片描述
从服务器发送给客户端的命令结构:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.2 网络连接命令

网络流定义了通过网络连接把音频,视频和数据消息流在客户端和服务器之间进行交换的通道。一个网络连接对象可以有多个
网络流,进而支持多个数据流。

客户端可以通过网络流发送到服务器的命令如下:

播放play
播放2 play2
删除流 deleteStream
关闭流 closeStream
接收音频 receiveAudio
接收视频 receiveVideo
发布 publish
定位 seek
暂停 pause

服务器通过发送 onStatus 命令给客户端来通知网络流状态的更新:

在这里插入图片描述

3.2.1 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 命令给客户端。之后,服务器发送音频和视频数据给客户端。

原文链接:
在这里插入图片描述

在这里插入图片描述

从客户端发送给服务器的命令结构如下:

  1. +--------------+----------+-----------------------------------------+
  2. | Field Name | Type | Description |
  3. +--------------+----------+-----------------------------------------+
  4. | Command Name | String | Name of the command. Set to "play". |
  5. +--------------+----------+-----------------------------------------+
  6. | Transaction | Number | Transaction ID set to 0. |
  7. | ID | | |
  8. +--------------+----------+-----------------------------------------+
  9. | Command | Null | Command information does not exist. |
  10. | Object | | Set to null type. |
  11. +--------------+----------+-----------------------------------------+
  12. | Stream Name | String | Name of the stream to play. |
  13. | | | To play video (FLV) files, specify the |
  14. | | | name of the stream without a file |
  15. | | | extension (for example, "sample"). To |
  16. | | | play back MP3 or ID3 tags, you must |
  17. | | | precede the stream name with mp3: |
  18. | | | (for example, "mp3:sample". To play |
  19. | | | H.264/AAC files, you must precede the |
  20. | | | stream name with mp4: and specify the |
  21. | | | file extension. For example, to play the|
  22. | | | file sample.m4v,specify "mp4:sample.m4v"|
  23. | | | |
  24. +--------------+----------+-----------------------------------------+
  25. | Start | Number | An optional parameter that specifies |
  26. | | | the start time in seconds. The default |
  27. | | | value is -2, which means the subscriber |
  28. | | | first tries to play the live stream |
  29. | | | specified in the Stream Name field. If a|
  30. | | | live stream of that name is not found,it|
  31. | | | plays the recorded stream of the same |
  32. | | | name. If there is no recorded stream |
  33. | | | with that name, the subscriber waits for|
  34. | | | a new live stream with that name and |
  35. | | | plays it when available. If you pass -1 |
  36. | | | in the Start field, only the live stream|
  37. | | | specified in the Stream Name field is |
  38. | | | played. If you pass 0 or a positive |
  39. | | | number in the Start field, a recorded |
  40. | | | stream specified in the Stream Name |
  41. | | | field is played beginning from the time |
  42. | | | specified in the Start field. If no |
  43. | | | recorded stream is found, the next item |
  44. | | | in the playlist is played. |
  45. +--------------+----------+-----------------------------------------+
  46. | Duration | Number | An optional parameter that specifies the|
  47. | | | duration of playback in seconds. The |
  48. | | | default value is -1. The -1 value means |
  49. | | | a live stream is played until it is no |
  50. | | | longer available or a recorded stream is|
  51. | | | played until it ends. If you pass 0, it |
  52. | | | plays the single frame since the time |
  53. | | | specified in the Start field from the |
  54. | | | beginning of a recorded stream. It is |
  55. | | | assumed that the value specified in |
  56. | | | the Start field is equal to or greater |
  57. | | | than 0. If you pass a positive number, |
  58. | | | it plays a live stream for |
  59. | | | the time period specified in the |
  60. | | | Duration field. After that it becomes |
  61. | | | available or plays a recorded stream |
  62. | | | for the time specified in the Duration |
  63. | | | field. (If a stream ends before the |
  64. | | | time specified in the Duration field, |
  65. | | | playback ends when the stream ends.) |
  66. | | | If you pass a negative number other |
  67. | | | than -1 in the Duration field, it |
  68. | | | interprets the value as if it were -1. |
  69. +--------------+----------+-----------------------------------------+
  70. | Reset | Boolean | An optional Boolean value or number |
  71. | | | that specifies whether to flush any |
  72. | | | previous playlist. |
  73. +--------------+----------+-----------------------------------------+

3.2.2 play: 播放2

与播放命令的不同之处在于,播放 2 命令可以在不修改播放内容时间线的前提下切换到一个不同码率的流。服务器包含了多个不同码率的流文件用于支持客户端的播放 2 请求。
有关 NetStreamPlayOptions 对象的公开属性的说明详见 AS3 语言的文档。

此命令的消息流如下所示:

从客户端发送给服务器的命令结构如下:

在这里插入图片描述

在这里插入图片描述

3.2.3 deleteStream: 删除流

如果需要销毁网络流对象,可以通过网络流发送删除流消息给服务器。

客户端发送给服务器的命令结构如下:
在这里插入图片描述
服务器接收到此消息后,不做任何回复。

3.2.4 receiveAudio: 接收音频

网络流发送此消息通知服务器,是否要发送音频数据给客户端
在这里插入图片描述
如果服务器接收到带有 fasle 标签的消息后,不做任何回复。如果接收到带有 true 标签的消息,服务器回复带有NetStream.Seek.Notify 和 NetStream.Play.Start 的消息给客户端。

3.2.5 receiveVideo: 接收视频

网络流发送此消息通知服务器,是否要发送视频数据给客户端。

客户端发送给服务器的命令结构如下:

在这里插入图片描述
如果服务器接收到带有 false 标签的消息后,不做任何回复。如果接收到带有 true 标签的消息,服务器回复带有
NetStream.Seek.Notify 和 NetStream.Play.Start 的消息给客户端。

3.2.6 publish: 发布

客户端发送此消息,用来发布一个有名字的流到服务器。其他客户端可以使用此流名来播放流,接收发布的音频,视频,以及其他数据消息。
在这里插入图片描述
服务器接收到此消息后,回复 onStatus 命令来标记发布的开始。

示例1:发布录制的视频

此示例阐述了发布者如果发布视频流到服务器。其他客户端可以订阅并播放此视频流。

在这里插入图片描述
示例2:从录制的流发布元数据

本示例展示了发布元数据的消息交换过程。

在这里插入图片描述

3.2.7 seek: 定位

客户端发送此消息来定位多媒体文件或播放列表的偏移(以毫秒为单位)。

客户端发送给服务器的命令结构如下:
在这里插入图片描述
当定位完成后,服务器回复 NetStream.Seek.Notify 状态消息给客户端。如果定位失败,将回复 _error 消息。

3.2.8 pause: 暂停

客户端发送此消息来通知服务器暂停或开始播放。

客户端发送给服务器的命令结构如下
在这里插入图片描述
当流暂停成功,服务器发送 NetStream.Pause.Notify 状态消息给客户端,如果流未暂停,服务器发送
NetStream.Unpause.Notify 状态消息给客户端。如果暂停失败,则发送 _error 消息。

3.3 Data Message ( 15 或 18)

Data Message(数据消息,Message Type ID = 15 或 18):传递一些元数据(Metadata,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用 AMF0 编码时,Message Type ID = 18,AMF3 则为15.

元数据包含了(音视频)数据的细节信息,像流的创建时间,时间点,主题等等.

3.4 Shared Object Message (16 或 19)

Shared Object Message(共享消息,Message Type ID = 16 或 19):表示一个 Flash 类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。AMF0 时 Message Type ID 为 19,AMF3 为 16。每个消息可以包含多个事件。

共享消息格式
在这里插入图片描述

下面是共享消息支持的事件类型:
在这里插入图片描述
在这里插入图片描述

3.5 Audio Message (8)

Audio Message(音频信息,Message Type ID = 8):音频数据

3.6 Video Message (9)

Video Message(视频信息,Message Type ID = 9):视频数据

3.7 Aggregate Message (22)

Aggregate Message(聚集信息,Message Type ID = 22):多个 RTMP 子消息的集合。
集合消息由消息头和消息内容组成。
消息内容由子消息组成,子消息由消息头,消息数据,回放指针组成。

在这里插入图片描述

集合消息的消息流 ID 覆盖此消息内的子消息流的ID。

集合消息和第一个子消息的时间戳之间的偏移量,用来将子消息的时间戳处理为流的时间刻度。每个子消息的时间戳可以通过添加偏移量来处理为正常的流时间。第一个子消息的时间戳应该和集合消息的时间戳相同,因此偏移量应该为零。

方向指针包含了以前的消息(包含头信息)的大小。集合消息包含此字段,一是为了适配 FLV 文件格式,二是为了回放定位。

使用集合消息有如下几种优势:

块流在一个块内至多可以携带一条完整的消息。使用集合消息之后,不仅可以增加块大小,同时还减少了发送的块数量;
集合消息的子消息可以连续的存储在内存中。当系统调用网络发送数据时更高效。

3.8 User Control Message Events (4)

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 块流层发送的,块大小的最大值应该满足在一个块里包含此消息。

用户控制事件支持如下类型:
在这里插入图片描述

3.8.1 Stream Begin

在这里插入图片描述

3.8.2 streamIsrecord

在这里插入图片描述

3.8.3 Stream EOF

在这里插入图片描述

下面是nginx-rmp文件结构

/* 点播相关*/

ngx_rtmp_dash_module

ngx_rtmp_mp4

ngx_rtmp_mp4

ngx_rtmp_mp4_module /* 主要支持rtmp MP4这块点播相关功能,支持seek操作*/

ngx_rtmp_flv_module /* 主要是flv文件格式的点播相关功能,支持seek操作 */

ngx_rtmp_play_module /* rtmp点播相关,支持本地,远程两种方式点播,远程点播http方式,支持flv,mp4两种格式 */

ngx_rtmp_record_module /* 视频录制默认是flv格式, 支持按时间,按文件大小,帧个数录制文件 */

/* hls文件切片相关*/

ngx_rtmp_hls_module

ngx_rtmp_mpegts

/* rtmp机制整体框架, 协议握手,初始化相关,数据收发*/

ngx_rtmp_handshake 主要是是三次握手相关

ngx_rtmp_handler 主要是数据接收recv,发送send,ping命令相关

ngx_rtmp_init 初始化连接相关的信息

ngx_rtmp_core_module 主要是rtmp协议核心配置相关.

ngx_rtmp rtmp配置解析,rtmp事件框架的初始化信息,注册事件回调函数(协议handler,amfhandler)

/* rtmp直播,以及统计、通知、控制相关功能*/

ngx_rtmp_receive 主要是rtmp协议数据接收这块

ngx_rtmp_send 数据发送这块,以及各种rtmp消息包发送封装的函数

ngx_rtmp_live_module主要处理接收音视频消息数据,以及ngx_rtmp_live_av中进行数据分发,从接收到发送给每个其他session

ngx_rtmp_netcall_module 主要是http请求相关部分

ngx_rtmp_notify_module 主要rtmp发送http请求,通知作用主要监听connect,disconnect,play,publish,close,record_done等相关事件

ngx_rtmp_relay_module 主要是rtmp提供回源请求拉流,以及转推,监听_result,_error, onStatus

ngx_rtmp_stat_module 主要是rtmp流状态信息可以输出到本地文件

ngx_rtmp_shared 主要是rtmp协议内存管理方面,其中用到了引用计数来管理内存

ngx_rtmp_bandwidth 主要是rtmp协议的带宽计费

ngx_rtmp_cmd_module rtmp消息命令相关play,publish

ngx_rtmp_codec_module rtmp音视频编解码信息相关

ngx_rtmp_control_module 主要是一些控制接口,录制开始/暂停,支持record,query,drop相关的接口

ngx_rtmp_eval 主要提供一些变量替换的函数接口,有内存泄漏

ngx_rtmp_amf ngx_rtmp_bitop 主要是封装读,写amf包信息

ngx_rtmp_access_module 监听play,publish事件,对ip做检查访问

ngx_rtmp_auto_push_module 多进程方案,推流来时,自动推流到其他worker进程

ngx_rtmp_exec_module 主要监听publish,play,close,record_done事件,然后进行执行脚本进行相应的业务,如转码

ngx_rtmp_limit_module 主要监听connect以及disconnect事件,通过计算连接数量来限制连接个数

ngx_rtmp_log_module 主要是rtmp日志相关,连接断开disconncet事件的时候,输出访问日志相关

业务相关扩展功能大体有四类:统计、通知、控制。它们的实现代价如下:
“统计”处理了数据收发部分的代码;
“通知”事件框架;
“控制”耦合了具体功能的调用;
还有其他一些异常消息情况

参考:
https://www.cnblogs.com/jimodetiantang/p/8974075.html
https://blog.csdn.net/weixin\_39371129/article/details/74576960

发表评论

表情:
评论列表 (有 0 条评论,487人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HTTP协议详细解读

    端口号的主要作用是表示一台计算机中特定的进程所提供的服务,即用来区分一个主机上的不同程序,每个程序在访问网络时,都会关联一个或者多个端口号,通过端口号区分当前的请求给谁,...

    相关 直播协议HTTP-FLV解读

    HTTP-FLV,即将音视频数据封装成FLV,然后通过HTTP协议传输给客户端。 这里首先要说一下,HLS其实是一个“文本协议”,而并不是一个流媒体协议。那么,什么样的协议才