iOS音视频直播--RTMP协议解析

概述

本文篇幅较长,主要介绍RTMP协议格式信令过程FLV tag三个部分,文档结构参考rtmp_specification_1.0,最后通过wireshark抓取项目推流过程介绍其中细节。
笔者项目背景是iOS推流端,所以研究的侧重点也在RTMP推流过程,后续涉及到拉流端再继续完善,如有错误感谢指出。
手打不易,感谢支持,转载请注明出处。

1. 简介

Adobe的实时消息传递协议(RTMP)在可靠的流传输协议(如TCP)之上提供双向消息多路服务,使得通信的两端传递并行的音视频流、数据流以及相关的时间信息。通过抽象出的chunk流,发送细粒度的数据包,并且可以为不同类型的信息分配不同的流通道,以便在传输能力受限时(如网络不佳)优先传输高优先级数据以减少网络延时。

2. 专有名词

  • Message stream:数据流中的逻辑通道。
  • Message stream ID:数据流中每一条message都有对应的msid来标识逻辑通道。
    默认0,推流端在createStream的_result回调时更新此id,之后发送消息使用新的msid。
  • Chunk:消息分块,消息真正在网络传输前会根据chunk size拆分成更小的数据包,chunk是RTMP协议层最基本的数据包,也是发送给下层协议的最小单位,音视频数据的chunks需保证时间戳递增(回退播放除外)。
  • Chunk stream:chunk流的逻辑通道。
  • Chunk stream ID:csid用来标识对应的chunk流。
    通常控制流csid为2,命令流为3,开发中发现音视频流csid可自定义,如音频流4,视频流6。
  • Action Message Format(AMF):用来序列化ActionScript对象图的二进制格式(功能类似json,相比json占用更少的字节),两个版本AMF0和AMF3。

3. 字节序,对齐及时间格式

除非特殊说明,字节序采用大端格式
默认字节对齐,如一个16-bit的字段填充数据时不足16bit需要用0补齐。
时间戳采用毫秒级无符号整型数。

4. Handshake

建立RTMP连接首先需要握手,由三个定长的chunk组成。

4.1 顺序要求

  • client必须收到s1才能发送c2;
  • client必须收到s2才能发送其他数据,如connect等连接操作;
  • server必须收到c0才能发送s0和s1;
  • server必须收到c1才能发送s2;
  • server必须收到c2才能发送其他数据;

如下图所示

实际开发中,为了尽量减少通信的次数,发送顺序可以优化成三步:

4.2 c0 and s0 Format

c0和s0都是8-bit的整型字段,如下图

Version(8 bit):该字段分别表示client/server支持的version,通常为3。

4.3 c1 and s1 Format

c1和s1都是1536字节,如下图

Time(4 bytes):时间戳,通常为0,是之后发送chunk流的参考时间;
Zero(4 bytes):恒为0;
Random data(1528 bytes):随机数;

4.4 c2 and s2 Format

c2和s2同样都是1536字节,如下图

Time(4 bytes):对于c2,填入对端发来的s1包里带的时间戳,s2同理;
Time2(4 bytes):对于c2,填入读取到s1包时刻的时间戳,s2同理,开发中发现可填0;
Random echo(1528 bytes):对于c2,填入对端发来的s1包里带的随机数,s2同理;
至此,握手阶段完成,可以发送message了。

5. RTMP Chunk Stream

Chunk Stream是对传输RTMP Chunk的流的抽象,可以对Control、Audio、Video等多路流同时传输,并在逻辑上区别开来。

5.1 Message Format

一个可以拆分成chunk发送的message,应该包含以下必要的字段:
Timestamp(3 bytes):message的时间戳;
Length(3 bytes):message的payload负载的长度,而非message的长度,对于control类型的消息是命令的长度,对于audio和video类型的消息是FLV audio/video tag的长度;
Type Id(1 byte):消息类型,预留的包含control类消息的各个细分命令,audio,video等,也可自定义id供RTMP协议之上的业务协议使用;
Message Stream ID(4 bytes):消息的唯一标识,简称msid,多路消息流复用相同的chunk流时根据这个id解复用,以便区分是否是同一个消息的chunk,小端格式存储

5.2 Chunking

RTMP发送的message需要根据chunk size拆分成chunk发送,而且必须在上一个chunk发送完成后才能发送下一个chunk,所以chunk是数据封包及发送的最小单位。每个chunk中带有(或间接找到)msid,接收端会按照这个id将chunk组装成message。
这么做的好处是避免一些数据量大但优先级较低的消息(如video)阻塞数据量小但优先级较高的消息(如audio和control),这样可以一定程度避免直播场景中因网络波动造成的卡顿现象。
并且chunk header的长度是变长的,根据场景可以选择type0-type3类型压缩header,从而减少分包造成的数据浪费。
chunk size默认128字节,在传输过程中,可通过Set Chunk Size 消息通知对端chunk拆分的最大值。更大的chunk size可减少cpu占用,但同时写入时间较长容易在低带宽网络时阻塞后面更重要的message。更小的chunk size可以减少阻塞问题,但是会引入更多额外的信息(chunk中的header),小量多次的传输也会造成发送的间断导致不能充分利用高带宽网络的优势。所以实际传输中要根据不同的网络带宽动态调整chunk size的大小,以在cpu利用率和网络带宽之间做出最佳权衡。

5.2.1 Chunk Format

每一个chunk由header和data组成,如下图

Basic Header(1-3 bytes):该字段包含chunk type和chunk stream id(简称csid),其中fmt决定了chunk的类型及message header的长度,占2 bit,而Basic header的长度取决于csid的数值大小,最少占1 byte;
Message Header(0,3,7 or 11 bytes):长度取决于Basic Header中的chunk type,有Type 0,1,2,3类型的header,注意message length字段指的是message body的长度(不包含header),而且如果被拆分成chunk,此字段填充拆分前message的body长度,而不是chunk的长度;
Extended Timestamp(0 or 4 bytes):当message header中的timestamp或timestamp delta大于0xFFFFFF时启用这个字段,存储直接或者间接时间戳,注意存储直接时间戳时要写入真实的时间戳;
Chunk Data(variable size):chunk的负载数据,最大为chunk size;

5.2.1.1 Chunk Basic Header

fmt(2 bits):该字段即是上文提到的chunk type,取值[0, 3],表示四种类型的Chunk Message Header;
csid:RTMP支持用户自定义[3, 65599]区间的csid,因为0、1、2为协议预留,预留的id可推算出Basic header长度,小端存储
0表示Basic Header总共占2个字节,csid在[64, 319]之间;
1表示占3个字节,csid在[64, 65599]之间;
2表示该chunk是控制信息,后面会讲到Protocol Control Message和User Control Message的csid必须为2。
而在[2, 63]区间的csid可以使用1字节的Basic Header,csid直接读出即可。

在[64, 319]区间的csid可使用2字节的header,实际的csid通过 the second byte + 64 计算得出。

在[64, 65599]区间的csid可使用3字节的header,其中csid占用2个字节,这就涉及到字节序,注意此时是小端存储,实际的csid通过 (the third byte)*256 + the second byte + 64 计算得出。

尽管可以随意自定义csid,但尽量使用较小的id以减小Header的长度。

5.2.1.2 Chunk Message Header

5.2.1.2.1 Type 0

Type 0的Chunk Message Header共11字节,在chunk stream开始的第一个chunk和时间戳有回退(如回退播放)的情况下必须使用Type 0。

timestamp(3 bytes):直接时间戳(区别于后续提到的间接时间戳 ts delta),如果时间戳大于等于16777215(二进制0xFFFFFF),该字段必须等于16777215,然后转存到4字节的Extended Timstamp字段中;
message length(3 bytes):注意这里是Message的长度,即拆分成chunk之前Message的payload负载的长度,而不是chunk里data的长度;
message type id(1 byte):消息类型,如8代表audio数据,9代表video,其他值代表细分的control消息,后面会详细讲到;
message stream id(4 bytes):通常相同的chunk stream中的所有消息都来自相同的message stream,虽然可以将不同的message streams传输到相同的chunk stream中,但这就抵消了Chunk Message Header头压缩的好处。所以开发直播推流时,audio和video的csid不一样,但msid一般都一样;

5.2.1.2.2 Type 1

共7字节,相比Type 0,省略msid字段,和前一个chunk共用msid。

timestamp delta(3 bytes):间接时间戳,表示和上一个chunk的时间戳差值,可用上一个的timestamp+delta计算得出。

5.2.1.2.3 Type 2

共3字节,省略msid和message length,和前一个chunk共用这俩字段,在发送定长的数据时,在第一个chunk之后使用这个类型。

5.2.1.2.4 Type 3

Type 3类型的chunk没有Message header。
当它跟在Type 0的chunk后面时,表示和前一个chunk的时间戳是相同的,也就是一个Message拆分成多个chunk时,后一个chunk和前一个chunk同属一个Message自然也就可以不用传Message header。
当它跟在Type 1或者Type2的chunk后面时,表示和前一个时间戳的差相同。如第一个chunk是Type 0,timestamp = 0,第二个chunk是Type 2,timestamp delta = 20,表示时间戳为0+20=20,第三个chunk是Type 3,则timestamp delta = 20,表示时间戳为20+20=40;

5.2.2 Examples

5.2.2.1 Example 1

下图展示了常见的audio消息流

分析:

  1. 第一个Message的chunk的chunk type为0,因为前面没有可参考的chunk,此时chunk的长度为Basic Header(1 byte)+ Message Header(11 bytes)+ Payload(32 bytes)= 44 bytes;
  2. 第二个Message的chunk可与前一个共用length、type id和msid,但是不可省略timestamp delta字段,所以chunk type为2,同理可得chunk长度为36 bytes;
  3. 第三个Message的chunk可以共用前一个的timestamp delta字段,所以chunk type为0,chunk长度为33 bytes;
    详见下图

5.2.2.2 Example 2

下图展示了如何将过长的message以128字节的chunk size拆分成多个chunks

分析:

  1. Payload length为307 > 128字节,所以需要将Message拆分成多个chunks,首个chunk使用Type 0;
  2. 之后的chunk由于是同一个Message拆分而来,以上字段都可共享,所以直接使用Type 3;
  3. 需要注意的是,第一个chunk的length需要传入整个message的负载长度即307;

以上两个例子可以看出,Type 3类型的chunk可以用在两个场景:

  1. 单个较大Message拆分的非首个chunk;
  2. 某个Message可以与之前的Message共享以上字段,其第一个chunk也可为Type 3;
    开发中场景1比较常见,比如视频帧大于chunk size(默认128 bytes)时,使用Type 3可有效减少chunk header长度。

5.3 Protocol Control Messages(协议控制消息)

在RTMP的chunk流中使用特殊的type id如1,2,3,5,6,代表协议控制消息,这类控制消息msid必须为0,csid必须为2,并且时间戳为0,接收端收到立即生效。

5.3.1 Set Chunk Size(Message Type ID = 1)

上文提到,rtmp消息需要以chunk size为单位封装成chunk包发送,因此接收端需要根据chunk size才能正确解包,所以双端都要记录对端的封包单位chunk size,默认128 bytes。
通信过程中可发送此消息通知对端更新其记录的本端的chunk size。比如client想发送131 bytes的音频数据(此时chunk size为128 bytes,不更新chunk size的话需要拆成两个chunk),此时client可通知对端,这边client的chunk size更新为131 bytes,之后发送一个data为131 bytes的chunk即可,server端收到Set Chunk Size之后更新chunk size即可正确解析之后到来的chunk。
双端的chunk size各自独立维护,可以不同。

如上图,payload的第一个bit恒为0。
chunk size(31 bits):可表示[1, 0x7FFFFFFF]区间,但是由于chunk size要小于Message的length,而Message length字段用3个字节存储,最大值为0xFFFFFF,所以实际可取值区间为[1, 0xFFFFFF]。

5.3.2 Abort Message(2)

发送数据过程中,发送端可发送Abort消息通知接收端丢弃当前未接收完的Message及忽略之后的消息,接收端根据Abort消息中的csid可丢弃对应chunk流中之后的所有数据。比如在发送端需要关闭时,发送此消息通知对端之后的数据可以不用处理了。

chunk stream ID(32 bits):payload中就一个字段csid,表示可忽略的chunk流。

5.3.3 Acknowledge(3)

当收到对端消息字节数等于window size时,接收端要回复一个ack告知对端可以继续发送数据。当然这个窗口大小也可以理解为接收端read buffer的大小,意为接收端在发送ack之前最多可以处理的数据大小。

sequence number(32 bits):接收端自handshake起接收到的总的数据量,单位字节。

5.3.4 Window Acknowledge Size(5)

对应于上文提到的ack,发送端可以发送此消息通知对端更新窗口大小,一般在音视频数据之前发送。
不同于chunk size,window size通常比较大,以容纳更多的数据在缓冲区中,并且双端的window size共同维护,保持相同

5.3.5 Set Peer Bandwidth(6)

限制对端的输出带宽,接收端收到该消息后,通过限制已发送但尚未收到ack的数据大小,来限制输出带宽。如果带宽与上次发送给发送端的window size不同的话,接收端需要重新发送Window Acknowledge Size消息。

limit type有以下取值:
0 - Hard:接收端立即将window size更新为window ack size。
1 - Soft:接收端可以更新window size,也可以保留原值,但是原值要小于window ack size。
2 - Dynamic:如果上次收到的Set Peer Bandwidth消息的limit type是Hard,这次也按Hard处理,否则可忽略此消息。

6. RTMP Command Messages

上文说到的Protocol Control Message(协议控制消息)是在RTMP Chunk Stream Protocol(RTMP Chunk流协议层),总的来说是对chunk流的管理。本章介绍在RTMP Stream layer(流协议层)的命令消息。

6.1 Types of Messages

6.1.1 Command Message(Message Type ID = 20,17)

命令消息,携带由AMF编码格式的命令,AMF0的type id是20,AMF3是17。例如connect,createStream,publish,play,pause等命令,对端会回复如_result,_error等状态,具体什么格式以及如何将命令和回调一一对应后面会详细介绍。命令消息中包含command name、transaction ID和params。

6.1.2 Data Message(18,15)

数据消息,传递音视频元数据和其他用户数据,AMF0的type id是18,AMF3是15。
这个是很有必要的,比如直播推流发送实际音视频数据之前,必须要先发送@setDataFrame命令,发送视频分辨率、帧率码率、音频采样率、音视频编码方式等元数据,以便接收端收到后续数据可以正确解析。

6.1.3 Share Object Message(19,16)

共享消息,多个客户端需要共享一些键值对时使用,AMF0是19,AMF3是16。

6.1.4 Audio Message(8)音频消息

6.1.5 Video Message(9)视频消息

6.1.6 Aggregate Message(22)

聚合消息,可以将多个子消息聚合到一个message中从而减少发送的chunk。而且子消息在内存中是连续存储的,在进行系统调用时效率更高。

6.1.7 User Control Message Events(4)

用户控制消息,client和server端可以发送该消息通知对端执行用户控制事件。此类消息msid必须为0,csid必须为2。负载格式(即RTMP Chunk Data)如下图


Event Type(16 bits):指定事件类型,共7种,如下图;
Event Data(变长):因为底层还要包装成chunk,所以为了不被拆分成多个chunk,需要保证Event Type+Event Data <= chunk size;

其中常见的有
Set Buffer Length:在server端处理数据流之前,client通知server每毫秒接收的流的大小;
Ping Request & Response:推流过程中server如果长时间收不到数据,会发送ping询问client是否可达,client状态正常时需要回复该消息表明网络可达;

6.2 Type of Commands

命令类型的消息有很多种,如上文所说都需要AMF编码,包含命令名称、事务ID和相关参数。如client端发送connect命令时需要包含要连接的应用名称作为参数,然后server端回复消息时带上收到的transaction ID对应此条消息。回复命令有_result,_error,或者其他如verifyClient,contactExternalServer的方法名。命令类型的csid通常为3。
命令分两大类,NetConnection和NetStream。

6.2.1 NetConnection Commands

连接命令,维护两端的连接状态,也可以实现RPC远程进程调用,接收端的回调为_result和_error,分别表成功和失败,有四个命令:

  • connect
  • call
  • close
  • createStream

6.2.1.1 connect

client发送connect命令请求连接对应的应用,格式如下

Command Object用到的键值对如下

audioCodecs取值如下

videoCodecs如下

videoFunction如下

编码类型如下

server端回复的消息结构如下

connect大致流程如下

  1. client发送连接命令,要求连接server的应用实例;
  2. server启动对应的应用,并发送Window Acknowledge Size告知client设置window size;
  3. server发送Set Peer Bandwidth限制client默认输出带宽,这个值一般等于上一步的window size;
  4. client收到Set Peer Bandwidth后,同样发送Window Acknowledge Size消息,一般等于之前收到的window size;
  5. server发送User Control Message(StreamBegin),表示准备接收message stream;
  6. server回复步骤1 _result或者_error告知client连接结果,并且包含相同的transcation ID(即1),同时参数携带如fmsVer表示flash server版本,capabilities表示可承载的连接数量,以及level,code,description等状态描述;

6.2.1.2 Call

Remote Procedure calls远程进程调用,也可理解为远程方法调用,格式如下

响应格式如下

6.2.1.3 createStream

client通过发送此命令创建发布音视频的逻辑通道,即message stream channel。之前提到msid默认为0,client收到createStream的回调之后,解析其中的msid并更新本地,自此之后发送数据的msid都为此id。client发送的格式如下

server回复的格式如下

6.2.2 NetStream Commands

流命令,对音视频流的状态管理,如FCPublish/publish发布流,play播放流等,接收端的回调为onStatus,有如下命令:

  • play
  • play2
  • deleteStream
  • closeStream
  • receiveAudio
  • receiveVideo
  • publish
  • seek
  • pause

onStatus回调的格式大致一致,如下图

6.2.2.1 play

拉流端向server端请求播放音视频流,可以多次调用以请求多个视频流,其中resset字段设置true用来播放和切换多路流,设置false会清除其他流仅播放当前请求的流。结构如下

play流程大致如下

  1. client接收到createStream _result之后,发送play命令请求播放视频流;
  2. server接收到play命令后,发送Set Chunk Size,后续发送的chunk包以此大小拆分;
  3. server紧接着发送User Control Message,如StreamIsRecorded和StreamBegin,通知client当前流的状态信息;
  4. server发送onStatus消息回复对应的play命令,如果流存在则回复NetStream.Play.Start,不存在则回复NetStream.Play.StreamNotFound,如果play命令设置了reset,server还会回复NetStream.Play.Reset。
  5. 之后server开始发送要播放的音视频数据。

6.2.2.2 play 2

和上文play命令都是请求播放音视频流,区别在于server会维护多中比特率的视频流,client可以调用play2命令切换。
结构如下,其中NetStreamPlayOptions使用ActionScript 3格式。

play2流程类似play,如下

6.2.2.3 deleteStream

对应于createStream,客户端在结束推流时发送deleteStream命令,server不需要回复此消息,结构如下图,其中Stream ID是要关闭的msid。

6.2.2.4 receiveAudio

client通知server是否要接收音频数据,结构如下

其中,如果flag为false,则服务端不需要回复。如果为true,server会回复NetStream.Seek.Notify和NetStream.Play.Start消息。

6.2.2.5 receiveVideo

client通知server是否要接收视频数据,结构如下

其中,如果flag为false,则服务端不需要回复。如果为true,server会回复NetStream.Seek.Notify和NetStream.Play.Start消息。

6.2.2.6 publish

上文说到,推流时client发送connect连接到server对应的应用,收到回调后发送createStream创建发布的msid,收到回调后就需要发送publish指定接下来推送的流名称,收到回调后就可以推音视频流了,拉流端就可以连接server拉取到对应名称的音视频流。结构如下

publish过程如下

6.2.2.7 seek

定位到视频的某个时间点,以毫秒为单位,server返回NetStream.Seek.Notify表成功,_error表失败,结构如下

6.2.2.8 pause

client发送pause通知server暂停或恢复播放,server回复NetStream.Pause.Notify表暂停成功,NetStream.Unpause.Notify表恢复成功,_error表暂停失败,结构如下

7. RTMP Video Payload

经过上文提到的handshake,connect,publish/play等一系列流程,接下来需要收发真正的音视频数据,这些数据放在message的payload里,拆分成chunk后即放在data字段。
以flv格式的视频为例,将编码后的音视频帧以FLV tag的AUDIODATA和VIDEODATA字段的格式存储到chunk的data中。

7.1.1 AUDIODATA

SoundFormat(4 bits):音频帧格式,一般为AAC;
SoundRate(2 bits):采样率,对于AAC取值恒为3;
SoundSize(1 bit):采样精度,对于压缩格式恒为1;
SoundType(1 bit):声道数,对于AAC恒为1;
SoundData(变长):音频帧数据,对于AAC取值为AACAUDIODATA;
对于AAC格式,SoundRate和SoundType取值恒定并不意味着采样率和声道数恒定,而是因为播放端可以通过解析AAC码流中的AudioSpecificConfig获得,所以忽略了这两个字段。

7.1.2 AACAUDIODATA

AAC码流需要传输两种类型的帧
AudioSpecificConfig:其中包含编码细分类别、采样率、声道数三个解码必要的参数;
raw frame data:AAC编码器输出的序列帧;

7.1.3 AudioSpecificConfig

object type(5 bits):编码细分类别,如1表AAC_Main,2表AAC_LC(低复杂度);
frequency index(4 bits):采样率,如3表48000Hz,4表44100Hz;
channel configuration(4 bits):声道配置,如1表单声道,2表双声道;
以上三字段5+4+4=13 bits,不足2字节的bit需要补位0以便字节对齐,如下

7.2.1 VIDEODATA

FrameType(4 bits):帧类型,如1表关键帧,2表非关键帧即PB帧;
CodecID(4 bits):编码格式,如7表avcC格式包装的H264序列帧;
这里说明下H264是编码格式,其输出的是可以在网络传输的NAL Unit,由于NALU长度不一,如果不加以封装则无法正确拆分多个NALU,主流包装格式有avcC和AnnexB两种;
VideoData(变长):视频帧数据,对于H264取值为AVCVIDEOPACKET;

7.2.2 AVCVIDEOPACKET

其中
AVCPacketType(8 bits):常用如0表包含sps/pps等解码必要数据;1表编码器输出的序列帧;
CompositionTime(3 bytes):单位毫秒,对于序列帧表示延迟解码间隔时间,因为B帧有解码延迟,若不存在B帧此值一般为0;
Data(变长):AVCDecoderConfigurationRecord或者avcC格式封装的H264序列帧;
对于传输NALU的情况,不同编码器的输出格式不同,如iOS的VideoToolBox输出avcC格式,Android的MediaCodec输出AnnexB格式,在RTMP场景下必要时需要做格式转换;

7.2.3 AVCDecoderConfigurationRecord

下面通过wireshark抓取的RTMP包分析

图中高亮部分即是AVCVIDEOPACKET,00表示Data字段存储的是AVCDecoderConfigurationRecord,其中
configurationVersion=01,版本号恒为1;
AVCProfileIndication=42,等于sps[1],表baseline,H264编码档次有baseline/main/high,其中baseline效率最高适用于实时通信;
profile_compatibility=e0,等于sps[2];
AVCLevelIndication=1f,等于sps[3],表auto;
lengthSizeMinusOne=ff,NALU单元头长度-1,即(lengthSizeMinusOne&0x03)+1=4字节;
numOfSequenceParameterSets=e1,sps个数,一般为1,(numOfSequenceParameterSets&0x1f)=1;
sequenceParameterSetLength=00 13,sps长度;
sequenceParameterSetNALUnit=27 42 e0 1f a9 18 3c 11 d8 0b 50 20 20 23 0a d7 bd f0 10,sps unit,可以看出AVCProfileIndication,profile_compatibility,AVCLevelIndication分别对应sps的1,2,3字节;
numOfPictureParameterSets=01,pps个数,一般为1;
pictureParameterSetLength=00 04,pps长度;
pictureParameterSetNALUnit=28 de 09 88,pps unit;
sps和pps一般都可通过API直接获得,其内部字段的意义属于编码器范畴,读者有兴趣可以自行深究:)

8. 附录

8.1 RTMP Messages

RTMP消息种类较多且繁琐,理解起来较为困难,笔者总结下图结构帮助理解。

8.2 wireshark分析推流过程

上图是推流项目中实际的交互流程,虽然与文档相比少了部分命令,基本也涵盖了主体流程,并且适用于多个平台的rtmp server,下面讲下流程细节:

8.2.1 Handshake

简化版的握手流程,相对简单不再赘述;

8.2.2 connect:c to s

下图高亮部分是rtmp header,由chunk basic header+chunk message header组成。
format:0表type 0 chunk,因为这是第一个chunk;
csid:Protocol Control Message和User Control Message为2,Command Messages通常为3,所以这里是3;
msid:上文说到createStream会创建消息流通道对应msid,所以一般在createStream之后msid才会有值;
transactionId:恒为1,之后会有_result与之对应;

8.2.3 Window Acknowledge Size:s2c

server通知client其缓冲窗口大小5000000字节。
csid:该消息属Protocol Control Message,所以为2;

8.2.4 Set Peer Bandwidth/Set Chunk Size/_result('NetConnection.Connect.Success'):s2c

可以看到三条消息的csid与上文说的规则一致,其中_result的transactionId是1,与connect对应。

8.2.5 Window Acknowledgement Size:c2s

client每收到window size大小的数据之后,都要返回ack,message body包含当前收到的字节数。

8.2.6 Set Chunk Size:c2s

client通知server其发送的chunk分包大小,自此client发的chunk以8192拆分,server发的chunk以4096拆分,双端各自维护chunk size且可以不同。

8.2.7 createStream:c2s

client发送的第二个command message,transactionId为2。

8.2.8 _result() of createStream:s2c

自此双端都明确创建了一个msid为1的逻辑通道,此后发送的消息msid都要为1。
这里由于底层TCP是全双工通信,可能导致之后紧接着发送的msid来不及更新为1,如FCPublish,这种情况也是允许的。

8.2.9 FCPublish:c2s

FCPublish没在官方文档中,可以看到与publish消息类似,猜测是为了版本兼容,读者了解的话麻烦告知:)。

8.2.10 onFCPublish:s2c

8.2.11 publish:c2s

此消息更新到了msid,自此之后的消息msid都为1。

8.2.12 onStatus('NetStream.Publish.Start'):s2c

publish属于NetStream Command Message,上文提到此类消息回调为onStatus且transactionId为0,client收到此消息后不能通过transactionId对应上,可通过其中的code对应。
自此发布流成功,接下来开始发送音视频相关数据。

8.2.13 @setDataFrame:c2s

发送音视频原数据,这些信息可在拉流端共享。

8.2.14 Audio Data:c2s

csid:音频和视频数据采用不同的chunk通道,即使用不同的csid,并且此csid可自定义;
format:当前是此chunk通道的首个chunk,所以使用type 0 chunk;
注意Audio data第一个字节是00,说明此chunk里包含的是AAC sequence header。

format:此chunk通道的非首个chunk,可使用type 1 chunk;
注意Audio data第一个字节是01,说明此chunk里包含的是音频序列帧。

8.2.15 Video Data:c2s

format:当前是此chunk通道的首个chunk,所以使用type 0 chunk;
注意Video data第一个字节是00,说明此chunk里包含的是AVC sequence header。

format:此chunk通道的非首个chunk,可使用type 1 chunk;
注意Video data第一个字节是01,说明此chunk里包含的是NAL Unit。
视频帧大于chunk size时会拆分成多个chunks,为减少数据量chunks中非首个chunk要使用Type 3类型,但是实测wireshark并不会显示Type 3的chunk,猜测内部实现把Type 3又整合成了Type 1,读者抓包时不必疑惑。

8.2.16 FCUnpublish:c2s

8.2.17 onFCUnpublish:s2c

8.2.18 closeStream:c2s

8.2.19 onStatus('NetStream.Unpublish.Success'):s2c

参考资料

1. RTMP
rtmp_specification_1.0.pdf
2. Action Message Format
AMF 0
AMF 3
3. FLV
video_file_format_spec_v10.pdf
4. AAC
Understanding_AAC
AudioSpecificConfig
5. H.264
H.264
AVC

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容