RTMP解析
Posted 不想打工O_o
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,从而实现信息的收发。
消息块 & 消息封包传输
RTMP 协议为了维持稳定连续传递,避免单次传输数据量问题,采用了传输层封包,数据流切片的实现形式。被用来对当前带宽进行划分和复用的最小传输单位,被称为 Chunk 即消息块。通常情况下,一个有效的消息,如果数据量超出当前 Chunk Size 的话,则会被拆分成多个分块来分批传输。通过指定首个 Chunk 和后续 Chunk 类型,以及 Chunk Header 其他标志性数据,来使当前被切割的消息,能够在对端得到有效的还原和执行。
消息块结构如图:
- 基础数据头(Basic Header):保存 CS ID(Chunk stream ID)、Chunk Type(决定 Msg Header 类型,也称为format message type 标志位 简称fmt)
- 消息数据头(Message Header):包含被发送消息的相关信息,类型Chunk Type决定
- 扩展时间戳(Extended Timestamp)(32-bits):消息头携带的时间戳扩展位
Basic Header
基础数据头,Chunk stream ID 可以配置为3~65599 这 65597 个不同标志中的其中一种。根据持有 Chunk stream ID 的长度,RTMP 规格将基础数据头分为3种:ID 在 2~63 范围内的 1-Byte 版;ID 在 64~319 范围内的 2-Byte 版;ID 在 64~65599 范围内的 3-Byte 版。
- fmt(2-bits):用来标志消息类型(Message Header的类型)
- cs id:用来区分消息信道
Message Header
消息数据头的类型,是由基础数据头中的 fmt 字段来标记的。总共分为4种类型:
Type | fmt | function | 作用 |
---|---|---|---|
Type 0 | 0x00 | Full 12-Byte RTMP chunk header(absolute timestamp) | 通信建立开始时,或切换到后台(标记当前PTS位置) |
Type 1 | 0x01 | Relativer 8-Byte RTMP chunk header(message stream ID is not inclued ,timestamp dalte) | 视频数据流标准 |
Type 2 | 0x02 | Relativer 4-Byte RTMP chunk header(only timestamp delta) | 音频数据流标准(无传输切割,流的完整性) |
Type 3 | 0x03 | Relativer 1-Byte RTMP chunk header(no “real” header,just the 1-Byte indlcating chunk header type & chunk stream ID) | 空数据标准,时差标准,延迟标记 |
- timestamp 消息时间戳(3-Bytes):标记当前消息绝对时间戳,有效位 24 bits,如果超出16777215(0xFFFFFF)则启用扩展时间戳(Extended Timestamp)。扩展位启用时,timestamp 位恒定为 16777215,通过还原 32 bits 的扩展位,加合为有效时间戳数据。时间戳在运用上对于不同消息类型会有区分,type 0 时为绝对时间戳,type 1/2 时为相对时间戳(时间差值)。
- msg length 消息头长度(1-Byte):携带 Chunk Header 数据长度信息(单位:Byte)
- msg length (cont) 消息体长度(2-Bytes):携带 Chunk Data 数据长度信息(单位:Byte)
- msg type 消息类型(1-Byte):携带消息类型信息,这是实际消息的类型,区别于消息头
- ms id 字段(1-Byte):消息归属消息流 ID 标志位,指定当前消息所属信道分类
- ms id (cont) 字段(3-Bytes):消息内容对应数据流 ID 标志位,指定数据所属数据流信道
扩展时间戳
扩展时间戳(Extended Timestamp)(32-bits)主要是配合 Message Header 内的时间戳使用,用来扩展可用时间范围。
RTMP Message Header
rtmp的协议的数据包,总的来讲分为两大部分,一部分是Rtmp Header,另一部分为Rtmp Body
RTMP Message Header,不是 Message Header,两个不是同一个东西。
RTMP header的长度不固定,可能的长度为12字节,8字节,4字节,1字节。
具体长度为多少个字节,由RTMP header数据包的第一个字节的高2位决定,第一个字节的低6位,命名为Chunk Stream ID,用来表示消息的级别。
往后的字段分别为3字节的时间戳,3字节的BodySize字段,表示RTMP Body所包含数据包的大小,1字节的Type ID字段表示消息类型ID,4字节的Stream ID,通常用以完成某些特定的工作,如使用ID为0的Stream来完成客户端和服务器的连接和控制,使用ID为1的Stream来完成视频流的控制和播放等工作。
12字节的header包含所有字段,8字节的没有Stream ID,4字节的只有开头的一个字节和时间戳,1字节的header只有开头一个字节
RTMP Message Body
RTMP数据包的序列化是按照AMF的格式进行的。
AMF定义中,首先使用一个字节来表示数据类型,在数据类型后面紧跟着的就是对应类型数据的长度,每一种类型长度字段所占用的字节数可能也不尽相同,是一种类似key-len-val的一种数据组织方式,不过,AMF中对于某些类型的数据,没有长度的选项,因为AMF中规定了该类型所占据的字节长度大小。
握手
RTMP是基于TCP的,在TCP层面有三次握手,当TCP连接建立后,再进行RTMP协议层次的握手。
握手的简单流程
在rtmp连接建立以后,服务端要与客户端通过3次交换报文完成握手。与其他握手协议不同,rtmp协议握手交换的数据报文是固定大小的,客户端向服务端发送的3个报文为c0、c1、c2,服务端向客户端发送的3个报文为s0、s1、s2。c0与s0的大小为1个字节,c1与s1的大小为1536个字节,c2与s2的大小为1536个字节。
发送顺序
- 建立连接后,客户端开始发送C0、C1块到服务器;
- 服务器端收到C0或C1后发送S0和S1;
- 当客户端收齐S0和S1之后,开始发送C2;
- 当服务端收齐C0和C1后,开发发送S2;
- 当客户端收到S2,服务端收到C2,握手完成。
在实际工程应用中,一般是客户端将C0、C1块同时发出,服务器在收到C1块之后同时将S0、S1、S2发给客户端。客户端收到S1之后,发送C2给服务端,握手完成。
握手数据包格式
- C0和S0占用一个字节,表示RTMP版本号。目前RTMP版本定义为3,0-2是早期的专利产品所使用的值,现已经废弃,4-31是预留值,32-255是禁用值。
- C1和S1占用1536个字节。包含4个字节的时间戳,4个字节的0和1528个字节的随机数。
- C2和S2占用1536个字节,包含4个字节的时间戳,4个字节的对端的时间戳(C2数据包为S1数据包的时间戳,S2为C1数据包的时间戳)。
connect消息
当rtmp客户端和rtmp服务端握手完成之后,客户端就会向服务端发送connect消息。connect消息的格式按照RTMP Header+RTMP Body的格式组织。
connect消息由四部分组成,首先是command name,用字符串类表示命令的类型,即"connect";在其之后紧跟着的是事务id,该值永远设为1;再之后是connect消息中承载的所有object,用来标识一些参数;再后是可选的用户参数。一般比较少用。command object部分就是按照AMF0的标准表示了多个字段,主要包含app、flashVer、tcUrl、fpad、capabilities、audiocodecs、videocodecs等字段。
- app:是application的缩写,代表客户端要链接到的rtmp服务器的应用程序
- flashVer表示flash播放器的版本号
- tcUrl:表示链接
- fpad:表示是否使用代理
- capabilities:用字符串表示object的类型,然后再跟着具体的值
- audioCodes:表示支持的音频编码格式
- videoCodec:表示支持的视频编码格式
connect的消息流
result消息
rtmp客户端发送connect消息之后,rtmp server会给客户端发送_result消息,通过该消息通知客户端连接状态(success/fail)。
一个_result消息由4部分组成,类型标识,transaction ID,properties,response related information,这四部分均以AMF格式进行编码。
- 类型标识:表示消息类型。
- transcationID:按照AMF0格式编码,0x00表示数字格式,其后用8个字节表示ID,对于connect消息的回复,此ID恒为1。
- properties:包含了两个Object类型的数据,一个fmsVer表示了FMS 服务器的版本信息,另外一个capabilites表示容量。
- response related information:包含许多关于connect连接的响应,以object类型进行组织。
connect后续消息
在connect消息流的图中可以看到服务器收到connect消息之后,会向客户端发送Window Acknowledgement Size消息和Set Peer Bandwidth消息。
Window Acknowledgement Size消息
Window Acknowledgement Size用来通知对端,如果收到该大小字节的数据,需要回复一个Acknowledgement消息,也就是ACK。
消息格式比较简单,组织结构是RTMP Header + RTMP Body,Body中直接使用4个字节表示要设置的大小。
Acknowlegement消息
Acknowlegement消息可以理解为Window Acknowlegement Size满足条件的触发消息,当一端收到的数据大小满足Window Acknowledgement Size设置的大小时,向对端发送Ack消息。
Acknowlegement消息,也按照RTMP Header + RTMP Body进行组织,其Body也直接使用4个字节,表示收到数据满足Window Acknowledgement Size的最后一个数据包的序列号。
交互过程中通常Window Acknowledgement Size消息多于Acknowlegement消息,因为只有满足大小的时候才回复Acknowlegement消息
Set Peer BandWidth消息
该消息里设置对端输出带宽,对端是通过设置Window Acknowledgement Size来实现流量控制的。超过Window Acknowledgement Size后未确认(不发送Acknowledgement)发送端将不再发送消息。所以对端收到set peer bandwidth后,如果之前发送的Window Acknowledgement Size和这里写的的Window Acknowledgement Size不一样,一般会发送一个Window Acknowledgement Size。
Set Peer Bandwidth消息还是按照RTMP Header + RTMP Body的格式组成。RTMP Body由两个字段组成,一个是Window acknowledgement size,占用4个字节;一个是limit type,表示限制的类型,可取的值为0(Hard),1(soft), 2(Dynamic)。
limity type表示了不同的限制策略:
- Hard:收到消息的一端需要按照消息中设置的Window size进行限制;
- Soft:收到消息的一端按照消息中设置的Window size或者已经生效的限制进行限制,以两者中较小的为准。
- Dynamic:如果之前的类型为Hard,则此消息也为Hard类型,否则忽略该类型。
StreamBegin
服务端发送Set Peer Bandwidth消息之后,客户端向服务端发送Window Acknowledgement Size消息,服务端再向客户端发送一条用户控制消息StreamBegin。
RTMP服务器发送StreamBegin以通知客户端流已经可以使用并且可以用于通信。默认情况下,从客户端成功接收到connect命令后,将在ID 0上发送StreamBegin。StreamBegin的数据字段占用4字节,其类型占用2个字节,所以RTMP Body部分总共占用6个字节(类型+数据)。其中数据字段代表已开始运行的流的流ID。
createStream
创建完RTMP连接之后就可以创建或者访问RTMP流,对于推流端,客户端要向服务器发送一个releaseStream命令消息,之后是createStream命令消息,对于拉流端,则要发送play消息请求视频资源。我们先来看看推流端的消息流程,当发送完createStream消息之后,解析服务器返回的消息会得到一个stream ID, 这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
createStream消息
createStream消息,RTMP客户端发送此消息到服务端,创建一个逻辑通道,用于消息通信。音频、视频、元数据均通过createStream创建的数据通道进行交互,而releaseStream与createStream相对应,为什么有的时候会在createStream之前先来一次releaseStream呢?这就像我们很多的服务实现中,先进行一次stop,然后再进行start一样。因为我们每次开启新的流程,并不能确保之前的流程是否正常走完,是否出现了异常情况,异常的情况是否已经处理等等,所以,做一个类似于恢复初始状态的操作,releaseStream就是这个作用。
createStream的整体架构,使用字符串类型标识命令的类型,恒为“createStream”;然后使用number类型标识事务ID;紧接着是command相关的信息,如果存在,用set表示,如果没有,则使用null类型表示。
_result/_error
客户端发送createStream请求之后,服务端会反馈一个结果给客户端,如果成功,则返回_result,如果失败,则返_error。
整体与createStream类似,commandName为固定为"_result"或者"_error",transcationID为createStream中的ID,此例中为2,comamndObject也是与createStream一样的组织方式。不同的是多了一个streamID,成功的时候,返回一个streamID,失败的时候返回失败的原因,类似于错误码。
releaseStream消息
release消息的组织结构,comandName + transactionID + commandObject + 流的一些用户名和密码信息等(可能没有)
commandName恒为“releaseStream”用以区分类型,transactionID指明要释放的stream,此处id为2,也就是我们前面createStream的id(在客户端请求下一个createStream的时候,就会先释放原有的stream),comandObject携带一些相关的信息(可能没有);userPass部分为针对流的一些用户名和密码的一些信息。
publish推流
publish消息
对于推流端,经过releaseStream,createStream消息之后,得到了_result消息之后,接下来客户端就可以发起publish消息。推流端使用publish消息向rtmp服务器端发布一个命名的流,发布之后,任意客户端都可以以该名称请求视频、音频和数据。
publish消息共有五个字段:
- commandName:使用string类型,表示消息类型(“publish”);
- transactionID:使用number类型表示事物ID;
- commandObject:对于publish消息,该部分为空,用null类型表示;
- publishName:发布的流的名称,使用string类型表示,比如我们发布到rtmp://192.168.1.101:1935/rtmp_live/test,则test为流名称,也可以省略,此时该字段为空字符;
- publishType:发布的流的类型,使用string类型表示,有3种类型,分别为live、record、append,record表示发布的视频流到rtmp服务器application对应的目录下会将发布的流录制成文件,append表示会将发布的视频流追加到原有的文件,如果原来没有文件就创建,live则不会在rtmp服务器上产生文件。
onStatus
客户端发送publish消息给rtmp服务端后,服务端会向客户端反馈一条消息,该消息采用了onStatus。
onStatus消息组成:
- command Name:表示消息类型,恒为“onStatus”;
- transaction ID:设为0;
- command Object:用null表示;
- info Object:使用object类型表示多个字段,一般有warn,status,code,description等状态。
SetDataFrame/OnMetaData
一般在客户端收到服务端返回的针对publish的onStatus消息之后,如果没有异常,推流端还会向服务器发送一条SetDataFrame的消息,其中包含onMetaData消息,这一条消息的主要作用是告诉服务端,推流段关于音视频的处理采用的一些参数,比如音频的采样率,通道数,帧率,视频的宽,高等信息。
消息的组织结构,RTMP Body部分首先以AMF0格式(string)表示SetDataFrame消息;然后以AMF0格式编码(string)表示onMetaData消息;最后描述具体的关于音视频相关的参数,该部分使用ECMA Array的类型来表示。
play拉流
在客户端发起createStream命令之后,客户端收到服务端反馈的_result消息,接下来客户端就可以向服务端发起请求播放的指令,这个指令就是play。
play消息流示意图
+-------------+ +----------+
| Play Client | | | Server |
+-------------+ | +----------+
| |Handshaking and Application| |
| | connect done | |
| | |
---+---- |---------Command Message(createStream) --------->|
Create | |
Stream | |
---+---- |<-------------- Command Message -----------------|
| (_result- createStream response) |
| |
---+---- |------------ Command Message (play) ------------>|
play | |
| |<---------------- SetChunkSize ------------------|
| | |
| |<----- User Control (StreamIsRecorded) ----------|
| | |
| |<-------- UserControl (StreamBegin) -------------|
| | |
| |<---- Command Message(onStatus-play reset) ------|
| | |
| |<---- Command Message(onStatus-play start) ------|
| | |
| |------------------ Audio Message---------------->|
| | |
| |------------------ Video Message---------------->|
| | |
|
|
Keep receiving audio and video stream till finishes
play的流程
- 客户端向服务端发送play指令之后,服务端收到之后向客户端发送SetChunkSize消息,实际场景中大都在服务器回复客户端connect消息的时候一起发送setChunkSize消息;
- 服务端向客户端发送StreamIsRecorded消息(实际场景中比较少见);服务端向客户端发送StreamBegin消息,向客户端指示流传输的开始;StreamIsRecoreded消息和StreamBegin消息组织结构比较简单,RTMP Body部分使用2个字节表示事件类型,StreamBegin的类型为0x00,4个字节表示StreamID。
- 客户端成功发送play请求后,服务端向客户端发送onStatus命令消息NetStream.Play.Start 和 NetStream.Play.Reset消息。其中NetStream.Play.Reset消息只有在客户端发送play消息的时候设置了reset标志的时候才会发。如果客户端请求播放的流不存在,服务端会返回onStatus命令消息NetStream.Play.StreamNotFound。
- 这些交互结束之后,服务端就会向客户端发送音频和视频数据,客户端就可以进行解码,然后渲染播放了。
play消息整体组织结构
- commandName:命令的名称,为“connect”;
- transaction ID:事务ID,用number类型表示;
- command Object:如果有,用object类型表示,如果没有,则使用null类型指明;
- stream Name:请求的流的名称,一般在url中application后面的字段,如rtmp://192.17.1.202:1935/rtmp_live/test,rtmp_live为application,test为流的名称;
- start:可选字段,使用number类型表示,指示开始时间,默认值为-2,表示客户端首先尝试命名为streamName的实时流(官方文档中说以秒单位,实际抓包文件中看到的单位应该是毫秒,要注意);
- duration:可选字段,用number类型表示,指定播放时间,默认值为-1,表示播放到流结束;
- reset:可选字段,用boolean类型表示,用来指示是否刷新之前的播放列表;
setChunkSize
setChunkSize消息结构也比较简单,RTMP Header中的typeID,用来表示消息类型,setChunkSize的类型为0x01。RTMP Body部分直接用4个字节表示chunk size。
onStatus-play start
如果没有任何异常情况,服务器会向客户端发送一个onStatus的状态,如果没有异常,该状态的描述为play start。
结构组织:
- onStatus消息由三部分组成:
- command Name:表示消息类型,恒为“onStatus”;
- transaction ID:设为0;
- command Object:用null表示;
- info Object:使用object类型表示多个字段,一般有warn,status,code,description等状态。
audio
音频数据也是按照RTMP Header + Rtmp Body的组织结构来进行封装的。因为rtmp是Adobe公司开发的协议,所以对自己东西当然是青睐有加,音频的数据的Body部分正是按照FLV的格式进行组装的。而Flv的封装以tag为单位来进行组织,对于音频数据,包含tagHeader + tagData,tagHeader占用一个字节,表明音频编码的相关参数,tagData为具体的音频编码数据。
tagHeader占用1个字节,高4比特,用于表示音频编码格式,在之后的2个bit,表示音频采样率,之后的1个bit表示采样位深度,可选值为0,1,0表示8比特深度,1表示16比特深度,之后的1个bit表示声道数的参数,可选值为0,1,0表示sndMono,1表示sndStereo。
videoData
视频数据也是按照Rtmp Header + Rtmp Body的组织结构,Body中打包的是经过压缩的视频数据。Body中打包视频数据的方式也与音频类似。首先用一个字节表示视频数据的header,之后是压缩后的视频数据(压缩后的数据是使用FLV的标准进行封装的)。
videoData中的header部分的组织结构相比音频比较简单,1个字节,高4位表示视频帧类型,低4位表示codecID。
- 帧类型:表示该帧视频是关键帧还是非关键帧。
- codecID:表示该帧的数据编码codecID。
vs2015编译ffmpeg 出现错误rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
vs2015编译ffmpeg(版本3.0.2)引用外部库文件librtmp出现以下错误:
rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 __imp__strtod
rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 __imp__sscanf
rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 __imp___snprintf
rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 __imp__rand
rtmp.lib(rtmp.obj) : error LNK2001: 无法解析的外部符号 __imp___strdup
rtmp.lib(log.obj) : error LNK2001: 无法解析的外部符号 __imp____iob_func
rtmp.lib(log.obj) : error LNK2001: 无法解析的外部符号 __imp___vsnprintf
rtmp.lib(hashswf.obj) : error LNK2001: 无法解析的外部符号 __imp__sprintf
rtmp.lib(hashswf.obj) : error LNK2001: 无法解析的外部符号 __imp___mktime64
rtmp.lib(hashswf.obj) : error LNK2001: 无法解析的外部符号 __imp__memchr
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 _sscanf
rtmp.lib(v3_utl.obj) : error LNK2001: 无法解析的外部符号 _sscanf
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 [email protected]
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 [email protected]
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 [email protected]
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 [email protected]
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 __vsnprintf
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 _vfprintf
rtmp.lib(ui_openssl.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(t1_enc.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(pem_lib.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(txt_db.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(d1_enc.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(cryptlib.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(d1_both.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(rsa_sign.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(s3_srvr.obj) : error LNK2001: 无法解析的外部符号 ___iob_func
rtmp.lib(pqueue.obj) : error LNK2001: 无法解析的外部符号 _printf
rtmp.lib(dso_win32.obj) : error LNK2001: 无法解析的外部符号 _sprintf
其中有部分原因是引用库librtmp内含的lib不是用vs2015编译的,解决方法之一用
vs2012或者更低版本重新编译librtmp,或者将所有的lib都用vs2015重新编译也可
解决问题.
如有问题交流请加流媒体/Ffmpeg/音视频 127903734。
以上是关于RTMP解析的主要内容,如果未能解决你的问题,请参考以下文章