阅读本文前,可以参考前面文章,便于分析和理解。
流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)
详解FLV格式分析
聊聊H264的 NALU
h264 NALU代码实战(1)
AAC ADTS实战(1)
AAC ADTS格式分析(1)
1.RTMP描述
RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的⼀种应⽤层的协议,⽤来解决多媒体数据传输流的多路复⽤(Multiplexing)和分包(packetizing)的问题。随着VR技术的发展,视频直播等领域逐渐活跃起来,RTMP作为业内⼴泛使⽤的协议也重新被相关开发者重视起来。
RTMP协议是应⽤层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。在基于传输层协议的链接建⽴完成后,RTMP协议也要客户端和服务器通过“握⼿”来建⽴基于传输层链接之上的RTMP Connection链接,在Connection链接上会传输⼀些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建⼀个Stream链接,⽤于传输具体的⾳视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做⾃⼰的格式化,这种格式的消息我们称之为RTMP Message,⽽实际传输的时候为了更好地实现多路复⽤、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是⼀个单独的Message,也可能是Message的⼀部分,在接收端会根据chunk中包含的data的⻓度,message id和message的⻓度把chunk还原成完整的Message,从⽽实现信息的收发。
2.RTMP握手
RTMP握手也分为简单握手和复杂握手,可以参考前面文章,有详细说明。流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)
要建⽴⼀个有效的RTMP Connection链接,⾸先要“握⼿”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进⾏有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这⼏点:
(1)客户端要等收到S1之后才能发送C2。
(2)客户端要等收到S2之后才能发送其他信息(控制信息和真实⾳视频等数据)。
(3)服务端要等到收到C0之后发送S1,涉及到s1对C0的数据拷贝。
(4)服务端必须等到收到C1之后才能发送S2,涉及到S2对C1的数据拷贝。
(5)服务端必须等到收到C2之后才能发送其他信息(控制信息和真实⾳视频等数据)。
如果每次发送⼀个握⼿chunk的话握⼿顺序会是这样(简单握手),如下图:
理论上来讲只要满⾜以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握⼿的身份验证功能的基础上尽量减少通信的次数,⼀般的发送顺序是这样的,这⼀点可以通过wireshark抓ffmpeg推流包进⾏验证,如下图:
3. RTMP Chunk Stream
Chunk Stream是对传输RTMP Chunk的流的逻辑上的抽象,客户端和服务器之间有关RTMP的信息都在这个流上通信。这个流上的操作也是我们关注RTMP协议的重点。
3.1 Message(消息)
这⾥的Message(flv的tag需要封装成Message)是指满⾜该协议格式的、可以切分成Chunk发送的消息,消息包含的字段如下:
(1)Timestamp(时间戳):消息的时间戳(但不⼀定是当前时间,后⾯会介绍),4个字节表示。
(2)Length(⻓度):是指Message Payload(消息负载)即⾳视频等信息的数据的⻓度,3个字节表示。
(3)TypeId(类型Id):消息的类型Id,1个字节。
(4)Message Stream ID(消息的流ID):每个消息的唯⼀标识,划分成Chunk和还原Chunk为Message的时候都是根据这个ID来辨识是否是同⼀个消息的Chunk的,4个字节,并且以⼩端格式存储。
3.2 Chunking(Message分块)
RTMP在收发数据的时候并不是以Message为单位的,⽽是把Message拆分成Chunk发送,⽽且必须在⼀个Chunk发送完成之后才能开始发送下⼀个Chunk(一般不会交替发送)。每个Chunk中带有Message ID代表属于哪个Message,接收端也会按照这个id来将chunk组装成Message。
为什么RTMP要将Message拆分成不同的Chunk呢?
这个在前面的文章也分析过,可以参考前面文章流媒体推拉流实战之RTMP协议分析(BAT面试官推荐) ,有以下2个原因:
(1)分包能够减小延时和阻塞,更能适应复杂网络环境变化。
(2) 通过拆分,数据量较⼤的Message可以被拆分成较⼩的“Message”,这样就可以避免优先级低(数据量大)的消息持续发送阻塞优先级⾼(数据量低)的数据,⽐如在视频的传输过程中,会包括视频帧,⾳频帧和RTMP控制信息,如果持续发送⾳频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦⼈的卡顿现象。同时对于数据量较⼩的Message,可以通过对Chunk Header的字段来压缩信息,从⽽减少信息的传输量。(具体的压缩⽅式会在后⾯介绍)
Chunk包大小如何设置?
设置Chunk包大小主要考虑2点,一个是CPU占有率,一个是网络发送时间和是否阻塞。这两点是矛盾关系,需要根据实际情况,折中考虑。Chunk的默认⼤⼩是128字节,在传输过程中,通过⼀个叫做Set Chunk Size的控制信息(⻅spec 5.4.1 )可以设置Chunk数据量的最⼤值,在发送端和接受端会各⾃维护⼀个Chunk Size(srs流媒体服务器默认是60000),可以分别设置这个值来改变⾃⼰这⼀⽅发送的Chunk的最⼤⼤⼩。
如果考虑CPU占有率减少多一点。那就选择大一点的Chunk,⼤⼀点的Chunk减少了计算每个chunk的发送时间,从⽽减少了CPU的占⽤率,但是它会占⽤更多的时间在发送上,尤其是在低带宽的⽹络情况下,很可能会阻塞后⾯更重要信息的传输。
如果考虑网络发送时间和是否阻塞多一点。那就选择小一点的Chunk,⼩⼀点的Chunk可以减少这种阻塞问题,但⼩的Chunk会引⼊过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利⽤⾼带宽的优势,因此CPU使用率会更高。
总的来说,Chunk小一点,不适合在高比特率的流中传输。在实际发送时应对要发送的数据⽤不同的Chunk Size去尝试,通过抓包分析等⼿段得出合适的Chunk⼤⼩,并且在传输过程中可以根据当前的带宽信息和实际信息的⼤⼩动态调整Chunk的⼤⼩,从⽽尽量提⾼CPU的利⽤率并减少信息的阻塞机率。折中考虑,弄清这2点,也是调优的关键。
3.3 Chunk Format(块格式)
这个格式分析,在前面文章分析过,可以参看前面文章。流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)
这里有个问题就是,⼀个流当中可以交错传输多种消息类型的Chunk,那么多个Chunk怎么标记同属于同⼀类Message的呢?
回答:通过Chunk Stream ID区分的,同⼀个Chunk Stream ID必然属于同⼀个Message。
还有个问题就是,Basic Header⾥⾯的chunk stream ID代表了通道,不同的Message 是否有不同的的chunk Stream ID?
回答:不同的Message的chunk Stream ID是不一样。RTMP流中视频和⾳频拥有单独的Chunk Stream ID,⽐如⾳频的cs id=20,视频的cs id=21。接收端接收到Chunk之后,根据cs id分别将⾳频和视频“拼成成Message消息”。
注意:每⼀种消息类型的起始chunk 的类型必须是 Type_0 类型的,表明我是⼀个新的消息的起始。
3.3.1 Basic Header(基本的头信息)
包含了chunk stream ID(流通道Id)和chunk type(chunk的类型),chunk stream id⼀般被简写为CSID,⽤来唯⼀标识⼀个特定的流通道,chunk type决定了后⾯Message Header的格式。Basic Header的⻓度可能是1,2,或3个字节,其中chunk type的⻓度是固定的(占2位,注意单位是位,bit),Basic Header是变⻓,其⻓度取决于CSID的⼤⼩,在⾜够存储这两个字段的前提下最好⽤尽量少的字节从⽽减少由于引⼊Header增加的数据量。
RTMP协议最多⽀持65597个⽤户⾃定义chunk stream ID,范围为[3,65599] ,ID 0, 1, 2被协议规范直接使⽤,其中ID值为0, 1分表表示了Basic Header占⽤2个字节和3个字节:
(1)当Basic Header为1个字节时,CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在 [0,63] 之间,其中⽤户可⾃定义的范围为 [3,63] ,实际是可以⽤2开始⽤。1个字节的Basic Header如下图:
(2)ID值0:代表Basic Header占⽤2个字节,CSID在 [64,319] 之间。
当Basic Header为2个字节时,结构如下图,CSID占只占8位,第⼀个字节除chunk type占⽤的bit都置为0,第⼆个字节⽤来表示CSID-64,8位可以表示 [0, 255] 共256个数,ID的计算⽅法为(第⼆个字节+64),范围为 [64,319]。
(3)ID值1:代表Basic Header占⽤3个字节,CSID在 [64,65599] 之间。
当Basic Header为3个字节时,结构如下图,以在此字段⽤3字节版本编码。ID的计算⽅法为(第三字节*256+第⼆字节+64)(Basic Header是采⽤⼩端存储的⽅式),范围为 [64,65599]。可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的 [64,319],但实际实现时还是应该秉着最少字节的原则使⽤2个字节的表示⽅式来表示 [64,319] 的CSID。
(4)ID值2:代表该chunk是控制信息和⼀些命令信息,后⾯会有详细的介绍。
3.3.2 Message Header(消息的头信息)
包含了要发送的实际信息(可能是完整的,也可能是⼀部分)的描述信息。Message Header的格式和⻓度取决于Basic Header的chunk type,共有4种不同的格式,这在前面的文章也有详细的分析过,流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)。由上⾯所提到的Basic Header中的fmt字段控制。其中第⼀种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使⽤的时候还是应该采⽤尽量少的字节表示相同意义的数据。以下按照字节数从多到少的顺序分别介绍这4种格式的Message Header。
Type=0: 占⽤11个字节
type=0时Message Header占⽤11个字节,其他三种能表示的数据它都能表示,但在chunk stream的开始的第⼀个chunk和头信息中的时间戳后退(即值与上⼀个chunk相⽐减⼩,通常在回退播放的时候会出现这种情况)的时候必须采⽤这种格式。
(1)timestamp(时间戳):占⽤3个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1, 当它的值超过这个最⼤值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接收端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。
(2)字段message length和message length(cont)(消息数据的⻓度),占⽤3个字节,表示实际发送的消息的数据如⾳频帧、视频帧等数据的⻓度,单位是字节。注意这⾥是Message的⻓度,也就是chunk属于的Message的总数据⻓度,⽽不是chunk本身Data的数据的⻓度。
(3)message type id(消息的类型id):占⽤1个字节,表示实际发送的数据的类型,如8代表⾳频数据、9代表视频数据。
(4)msg stream id(消息的流id):占⽤4个字节,表示该chunk所在的流的ID,和Basic Header的CSID⼀样,它采⽤⼩端存储的⽅式。
Type = 1:占⽤7个字节
type=1时Message Header占⽤7个字节,省去了表示msg stream id的4个字节,表示此chunk和上⼀次发的chunk所在的流相同,如果在发送端只和对端有⼀个流链接的时候可以尽量去采取这种格式。
(1)timestamp delta:占⽤3个字节,注意这⾥和type=0时不同,存储的是和上⼀个chunk的时间差。类似上⾯提到的timestamp,当它的值超过3个字节所能表示的最⼤值时,三个字节都置为1,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时就会去Extended timestamp中解析时机的与上次时间戳的差值。
Type = 2:占⽤3个字节
type=2时Message Header占⽤3个字节,相对于type=1格式⼜省去了表示消息⻓度的3个字节和表示消息类型的1个字节,表示此chunk和上⼀次发送的chunk所在的流、消息的⻓度和消息的类型都相同。余下的这三个字节表示timestamp delta,使⽤同type=1。
Type = 3:占⽤0字节
0字节!!!好吧,它表示这个chunk的Message Header和上⼀个是完全相同的,⾃然就不⽤再传输⼀遍了。当它跟在Type=0的chunk后⾯时,表示和前⼀个chunk的时间戳都是相同。什么时候连时间戳都相同呢?就是⼀个Message拆分成了多个chunk,这个chunk和上⼀个chunk同属于⼀个Message。⽽当它跟在Type=1或者Type=2的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种type对⽐
4种类型对比,在前面文章也有讲解,也可以参考前面文章。流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)
3.3.3 Extended Timestamp(扩展时间戳)
上⾯我们提到在chunk中会有时间戳timestamp和时间戳差timestamp delta,并且它们不会同时存在,只有这两者之⼀⼤于3个字节能表示的最⼤数值0xFFFFFF=16777215时,才会⽤这个字段来表示真正的时间戳,否则这个字段为0。扩展时间戳占4个字节,能表示的最⼤数值就是0xFFFFFFFF=4294967295。当扩展时间戳启⽤时,timestamp字段或者timestamp delta要全置为0xFFFFFF,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。
注意:扩展时间戳存储的是完整值,⽽不是减去时间戳或者时间戳差的值。
3.3.4 Chunk Data(块数据)
⽤户层⾯上真正想要发送的与协议⽆关的数据,⻓度在(0,chunkSize]之间。
3.3.5 chunk表示例1
这个例⼦显示了⼀个简单的⾳频信息流。这个例⼦演示了信息的冗余。
看懂下面这段分析,就可以了解到,Message是怎样做到变长。
(1)第⼀个Message的chunk的Chunk Type为0,因为它没有前⾯可参考的chunk,timestamp为1000,表示时间戳。type为0的header占⽤11个字节,假定chunkstreamId为3<127,因此BasicHeader占⽤1个字节,再加上Data的32个字节,因此第⼀个chunk共44=11+1+32个字节。
(2)第⼆个chunk和第⼀个chunk的CSID,TypeId,Data的⻓度都相同,因此采⽤Chunk Type=2,timestamp delta=1020-1000=20,因此第⼆个chunk占⽤36=3+1+32个字节。
(3)第三个chunk和第⼆个chunk的CSID,TypeId,Data的⻓度和时间戳差都相同,因此采⽤Chunk Type=3省去全部Message Header的信息,占⽤33=1+32个字节。
(4)第四个chunk和第三个chunk情况相同,也占⽤33=1+32个字节。
最后实际发送的chunk如下:
3.3.6 Chunk数据量过大
如果一个Message消息因为太⻓,以⾄于⽆法适⽤⼀个128字节的chunk,从⽽被分解成多个chunk。
(1)注意到Data的Length=307>128,因此这个Message要切分成⼏个chunk发送,第⼀个chunk的Type=0,Timestamp=1000,承担128个字节的Data,因此共占⽤140=11+1+128个字节。
(2)第⼆个chunk也要发送128个字节,其他字段也同第⼀个chunk,因此采⽤Chunk Type=3,此时时间戳也为1000,共占⽤129=1+128个字节
(3)第三个chunk要发送的Data的⻓度为307-128-128=51个字节,还是采⽤Type=3,共占⽤1+51=52个字节。
最后实际发送的chunk如下:
从两个例⼦中注意到,Type = 3的chunk可以⽤在两种不同的⽅式中。第⼀种是指定消息的继续。第⼆种是指定⼀个新的消息的开始,它的头可以来⾃于现有的状态数据。
3.4 协议控制消息(Protocol Control Message)
在RTMP的chunk流会⽤⼀些特殊的值来代表协议的控制消息,它们的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1,2,3,5,6,具体代表的消息会在下⾯依次说明。控制消息的接收端会忽略掉chunk中的时间戳,收到后⽴即⽣效。
Set Message Type ID=1
设置chunk中Data字段所能承载的最⼤字节数,默认为128Bytes,通信过程中可以通过发送该消息来设置chunk Size的⼤⼩(不得⼩于128Bytes),⽽且通信双⽅会各⾃维护⼀个chunkSize,两端的chunkSize是独⽴。
⽐如当A想向B发送⼀个200Bytes的Message,但默认的chunkSize是128Bytes,因此就要将该消息拆分为Data分别为128Bytes和72Bytes的两个chunk发送。如果此时先发送⼀个设置chunkSize为256Bytes的消息,再发送Data为200Bytes的chunk,本地不再划分Message,B接收到Set Chunk Size的协议控制消息时会调整的接受的chunk的Data的⼤⼩,也不⽤再将两个chunk组成为⼀个Message,所以根据实际的情况,设置客户端和服务端的Chunk Size值。
在实际写代码的时候⼀般会把chunk size设置的很⼤,有的会设置为4096,FFMPEG推流的时候设置的是 60*1000,这样设置的好处是避免了频繁的拆包组包,占⽤过多的CPU。网络不佳时,但发送可能会耗时。
以下为代表Set Chunk Size消息的chunk的Data:
其中第⼀位必须为0,chunk Size占31个位,最⼤可代表2147483647=0x7FFFFFFF=2^31-1,但实际上所有⼤于16777215=0xFFFFFF的值都⽤不上,因为chunk size不能⼤于Message的⻓度,表示Message的⻓度字段是⽤3个字节表示,最⼤只能为0xFFFFFF。
Abort Message (ID=2)
Abort Message(Message Type ID=2):当⼀个Message被切分为多个chunk,接受端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接受端接收到这个消息后要丢弃这些不完整的chunk。Data数据中只需要⼀个CSID,表示丢弃该CSID的所有已接收到的chunk。
Acknowledgement (ID=3)和Window Acknowledgement Size (ID=5)
Window Acknowledgement Size⽤于设置窗⼝确认⼤⼩,Acknowledgement是窗⼝确认消息。会话开始时,双⽅都要先对端发送Window Acknowledgement Size,⽤于指明期望获得确认的⼤⼩。当⼀端收到内容⼤⼩超过Window Acknowledgement Size,就要向对⽅发送Acknowledgement。详细流程如下:
(1)会话开始计算收到byte个数的时间点是收到Window Acknowledgement Size消息开始。
(2)byte size不包括tcp包头,应该是chunk的⼤⼩,即从tcp 的recv函数中获得的内容⼤⼩。
(3)双⽅都要向对⽅发送Window Acknowledgement Size和Acknowledgement。
(4)发送端发送完Window Acknowledgement Size消息后,没有收到Acknowledgement是不再发送进⼀步的消息——这样会容易引起错误,导致再也发送不出消息了。
对于拉流端,⼀般在收到av_createStream后,接着play,然后发送Acknowledgement 以让服务器继续发送数据。用于相互确认发送窗口大小。
Set Peer Bandwidth (ID=6)
Set Peer Bandwidth(Message Type ID=6):限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的⼤⼩来限制发送端的发送带宽。如果消息中的Window ACK Size与上⼀次发送给发送端的size不同的,要回馈⼀个WindowAcknowledgement Size的控制消息。同样,也是要相互确定大小。
详细流程如下:
(1)Hard(Limit Type=0):接收端应该将Window Ack Size设置为消息中的值。
(2)Soft(Limit Type=1):接收端可以将Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size⼩与该控制消息中的Window Ack Size)
(3)Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。
4. 不同类型的RTMP Message
(1)Command Message(命令消息,Message Type ID=17或20):表示在客户端和服务器间传递,在对端执⾏某些操作的命令消息,如connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息,publish表示开始向对⽅推流,接收端接到命令后,准备好接收对端发送的流信息,后⾯会对⽐较常⻅的Command Message具体介绍。当信息使⽤AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17。
(2)Data Message(数据消息,Message Type ID=15或18):传递⼀些元数据(MetaData,⽐如视频名,分辨率等等)或者⽤户⾃定义的⼀些消息。当信息使⽤AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15
(3)Shared Object Message(共享消息,Message Type ID=16或19):表示⼀个Flash类型的对象,由键值对的集合组成,⽤于多客户端,多实例时使⽤。当信息使⽤AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16。
(4)Audio Message(⾳频信息,Message Type ID=8):⾳频数据。
(5)Video Message(视频信息,Message Type ID=9):视频数据。
(6)Aggregate Message (聚集信息,Message Type ID=22):多个RTMP⼦消息的集合。
(7)User Control Message Events(⽤户控制消息,Message Type ID=4):告知对⽅执⾏该信息中包含的⽤户控制事件,⽐如Stream Begin事件告知对⽅流信息开始传输。和前⾯提到的协议控制信息(Protocol Control Message)不同,这是在RTMP协议层(更上层),⽽不是在RTMP chunk流协议层的,这个很容易弄混。该信息在chunk流中发送时,Message Stream ID=0,Chunk Stream Id=2,Message Type Id=4。
以下详细介绍这几种情况。
4.1 Command Message(命令消息,Message Type ID=17 或20)
实际使⽤时只是使⽤了ID=20,发送端发送时会带有命令的名字,如connect,TransactionID表示此次命令的标识,Command Object表示相关参数。接受端收到命令后,会返回以下三种消息中的⼀种:
(1)_result消息表示接受该命令,对端可以继续往下执⾏流程。
(2)_error消息代表拒绝该命令要执⾏的操作。
(3)method name消息代表要在之前命令的发送端执⾏的函数名称。
注意:这三种回应的消息都要带有收到的命令消息中的Transaction Id,表示本次的回应作⽤于哪个命令。可以认为发送命令消息的对象有两种,⼀种是NetConnection,表示双端的上层连接,⼀种是NetStream,表示流信息的传输通道,控制流信息的状态,如Play播放流,Pause暂停。
4.1.1 NetConnection Commands(连接层的命令)
⽤来管理双端之间的连接状态,同时也提供了异步远程⽅法调⽤(RPC)在对端执⾏某⽅法,以下是常⻅的连接层的命令:
connect:⽤于客户端向服务器发送连接请求,握⼿之后先发送⼀个connect 命令消息,这些信息是以AMF格式发送的,消息的结构如下:
第三个字段中的Command Object中会涉及到很多键值对,使⽤时可以参考协议的官⽅⽂档。
消息的回应有两种,_result表示接受连接,_error表示连接失败。
以下是连接命令对象中使⽤的名称-值对的描述:
Call:⽤于在对端执⾏某函数,即常说的RPC:远程进程调⽤,消息的结构如下:
如果消息中的TransactionID不为0的话,对端需要对该命令做出响应,响应的消息结构如下:
Create Stream:创建传递具体信息的通道,从⽽可以在这个流中传递具体信息,传输信息单元为Chunk。当发送完createStream消息之后,解析服务器返回的消息会得到⼀个stream ID, 这个ID也就是以后和服务器通信的 message stream ID, ⼀般返回的是1,不固定。
4.1.2 NetStream Commands(流连接上的命令)
Netstream建⽴在NetConnection之上,通过NetConnection的createStream命令创建,⽤于传输具体的⾳频、视频等信息。在传输层协议之上只能连接⼀个NetConnection,但⼀个NetConnection可以建⽴多个NetStream来建⽴不同的流通道传输数据(一定要记住,这在wireshark上可以得到)。
以下会列出⼀些常⽤的NetStream Commands,服务端收到命令后会通过onStatus的命令来响应客户端,表示当前NetStream的状态。
onStatus命令的消息结构如下:
play(播放):由客户端向服务器发起请求从服务器端接受数据(如果传输的信息是视频的话就是请求开始播流),可以多次调⽤,这样本地就会形成⼀组数据流的接收者。注意其中有⼀个reset字段,表示是覆盖之前的播流(设为true)还是重新开始⼀路播放(设为false)。
play命令的结构如下:
play2(播放):和上⾯的play命令不同的是,play2命令可以将当前正在播放的流切换到同样数据但不同⽐特率的流上,服务器端会维护多种⽐特率的⽂件来供客户端使⽤play2命令来切换。
deleteStream(删除流):⽤于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此路流。
receiveAudio(接收⾳频):通知服务器端该客户端是否要发送⾳频。
receiveAudio命令结构如下:
receiveVideo(接收视频):通知服务器端该客户端是否要发送视频
receiveVideo命令结构如下:
publish(推送数据):由客户端向服务器发起请求推流到服务器。
publish命令结构如下:
seek(定位流的位置):定位到视频或⾳频的某个位置,以毫秒为单位。
seek命令的结构如下:
pause(暂停):客户端告知服务端停⽌或恢复播放。
如果Pause为true即表示客户端请求暂停的话,服务端暂停对应的流会返回NetStream.Pause.Notify的onStatus命令来告知客户端当前流处于暂停的状态,当Pause为false时,服务端会返回NetStream.Unpause.Notify的命令来告知客户端当前流恢复。如果服务端对该命令响应失败,返回_error信息。
pause命令的结构如下:
5. 代表流程
推流流程
推流流程,在前面的文章已经有了很深入的介绍。流媒体推拉流实战之RTMP协议分析(BAT面试官推荐)
现贴下流程。如下图:
拉流播放流程如下:
6. 新⼿学习建议
如果读者仔细读完了上⾯讲的RTMP协议,想必会觉得RTMP协议⾮常繁琐,事实也确实是这样,RTMP协议中充斥着很多冗余的字段,⽐如三次握⼿中的时间戳的校对,还有⼀些特殊的命令,如FCPublish、UnFCPublish等,但在实际实现中为了保证更⼤兼容性通常还是要处理这些看似多余的命令。加上Adobe对RTMP协议的实现细节有些并没有按照协议来或者协议中,没有写清楚⾃⼰搞了⼀套实现,其他应⽤开发时还要兼容Adobe错误的实现,从⽽使的RTMP也⼀直为开发者所诟病。但不管怎样,RTMP确实提供了⼀种能够全⾯并且实现简单的协议来保证流信息的传输,这⽅⾯暂时还没有⼀种更完善更简洁的协议能够取代它在视频流开发中的地位。新⼈⼀开始接触RTMP的时候肯定会觉得头⼤,这也是RTMP协议不简洁的后果。建议读者在学习时先过⼀遍协议理解⼤概的概念和流程,然后对照wireshark抓的包,和协议进⾏⽐对,这样将理论和实践结合,应该会理解的更快⼀点。
7.源码参考
源码主要来源如下开源库:
(1). librtmp
(2). ffmpeg rtmppkt.h
(3). srs
csid是⾃定义的,主要不使⽤保留的0,1即可,⽐如ffmpeg的rtmppkt.h。对于chunk size的设置,ffmpeg保持和服务器⼀致。
/*** channels used to for RTMP packets with different purposes (i.e. da ta, network * control, remote procedure calls, etc.) */ enum RTMPChannel { RTMP_NETWORK_CHANNEL = 2, ///< channel for network-related mess ages (bandwidth report, ping, etc) RTMP_SYSTEM_CHANNEL, ///< channel for sending server contr ol messages RTMP_AUDIO_CHANNEL, ///< channel for audio data RTMP_VIDEO_CHANNEL = 6, ///< channel for video data RTMP_SOURCE_CHANNEL = 8, ///< channel for a/v invokes };
librtmp
RTMP_Write适合将FLV⽂件帧进⾏发送,RTMP_Read适合⽤来dump RTMP码流。
srs源码
srs_kernel_flv.hpp
message type相关。
// 5. Protocol Control Messages // RTMP reserves message type IDs 1-7 for protocol control messages. // These messages contain information needed by the RTM Chunk Stream // protocol or RTMP itself. Protocol messages with IDs 1 & 2 are 5 // reserved for usage with RTM Chunk Stream protocol. Protocol messa ges6 // with IDs 3-6 are reserved for usage of RTMP. Protocol message wit h ID 7 // 7 is used between edge server and origin server. 8 #define RTMP_MSG_SetChunkSize 0x01 9 #define RTMP_MSG_AbortMessage 0x02 10 #define RTMP_MSG_Acknowledgement 0x03 11 #define RTMP_MSG_UserControlMessage 0x04 12 #define RTMP_MSG_WindowAcknowledgementSize 0x05 13 #define RTMP_MSG_SetPeerBandwidth 0x06 14 #define RTMP_MSG_EdgeAndOriginServerCommand 0x07 15 // 3. Types of messages 16 // The server and the client send messages over the network to 17 // communicate with each other. The messages can be of any type whic h 18 // includes audio messages, video messages, command messages, shared 19 // object messages, data messages, and user control messages. 20 // 3.1. Command message 21 // Command messages carry the AMF-encoded commands between the clien t 22 // and the server. These messages have been assigned message type va lue 23 // of 20 for AMF0 encoding and message type value of 17 for AMF3 24 // encoding. These messages are sent to perform some operations like 25 // connect, createStream, publish, play, pause on the peer. Command 26 // messages like onstatus, result etc. are used to inform the sender 27 // about the status of the requested commands. A command message 28 // consists of command name, transaction ID, and command object that 29 // contains related parameters. A client or a server can request Rem
srs_rtmp_stack.hpp
// The amf0 command message, command name macros #define RTMP_AMF0_COMMAND_CONNECT "connect" #define RTMP_AMF0_COMMAND_CREATE_STREAM "createStream" #define RTMP_AMF0_COMMAND_CLOSE_STREAM "closeStream"#define RTMP_AMF0_COMMAND_PLAY "play" #define RTMP_AMF0_COMMAND_PAUSE "pause"#define RTMP_AMF0_COMMAND_ON_BW_DONE "onBWDone"#define RTMP_AMF0_COMMAND_ON_STATUS "onStatus"#define RTMP_AMF0_COMMAND_RESULT "_result"#define RTMP_AMF0_COMMAND_ERROR "_error" #define RTMP_AMF0_COMMAND_RELEASE_STREAM "releaseStream" #define RTMP_AMF0_COMMAND_FC_PUBLISH "FCPublish"#define RTMP_AMF0_COMMAND_UNPUBLISH "FCUnpublish" #define RTMP_AMF0_COMMAND_PUBLISH "publish" #define RTMP_AMF0_DATA_SAMPLE_ACCESS "|RtmpSampleAccess" // The signature for packets to client. #define RTMP_SIG_FMS_VER "3,5,3,888" #define RTMP_SIG_AMF0_VER 0 #define RTMP_SIG_CLIENT_ID "ASAICiss" // The onStatus consts.#define StatusLevel "level" #define StatusCode "code" #define StatusDescription "description" #define StatusDetails "details"#define StatusClientId "clientid"// The status value #define StatusLevelStatus "status" // The status error #define StatusLevelError "error" // The code value #define StatusCodeConnectSuccess "NetConnection.Conne ct.Success" #define StatusCodeConnectRejected "NetConnection.Conne ct.Rejected" #define StatusCodeStreamReset "NetStream.Play.Rese t" #define StatusCodeStreamStart "NetStream.Play.Star t" #define StatusCodeStreamPause "NetStream.Pause.Not ify" #define StatusCodeStreamUnpause "NetStream.Unpause.N otify" #define StatusCodePublishStart "NetStream.Publish.S tart" #define StatusCodeDataStart "NetStream.Data.Star t" #define StatusCodeUnpublishSuccess "NetStream.Unpublis h.Success"
重点参考一些比较好用源码和有关RTMP的知识。
https://github.com/zkzszd/NativeCodec.git
https://github.com/Cooliodtryl/upstream-h.264-es-file-as-rtmp-.git
https://github.com/ireader/media-server
https://github.com/medooze/media-server
直播推流实现RTMP协议的⼀些注意事项: https://www.cnblogs.com/lidabo/p/7232594.html
RTMP 协议规范(中⽂版) :https://www.cnblogs.com/Kingfans/p/7083100.html
本篇文章就分享到这里,欢迎关注,点赞,收藏,转发,评论区讨论 。
更多技术知识,欢迎关注 微信公众号 “记录世界 from antonio”