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种类型:

Typefmtfunction作用
Type 00x00Full 12-Byte RTMP chunk header(absolute timestamp)通信建立开始时,或切换到后台(标记当前PTS位置)
Type 10x01Relativer 8-Byte RTMP chunk header(message stream ID is not inclued ,timestamp dalte)视频数据流标准
Type 20x02Relativer 4-Byte RTMP chunk header(only timestamp delta)音频数据流标准(无传输切割,流的完整性)
Type 30x03Relativer 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的流程

  1. 客户端向服务端发送play指令之后,服务端收到之后向客户端发送SetChunkSize消息,实际场景中大都在服务器回复客户端connect消息的时候一起发送setChunkSize消息;
  2. 服务端向客户端发送StreamIsRecorded消息(实际场景中比较少见);服务端向客户端发送StreamBegin消息,向客户端指示流传输的开始;StreamIsRecoreded消息和StreamBegin消息组织结构比较简单,RTMP Body部分使用2个字节表示事件类型,StreamBegin的类型为0x00,4个字节表示StreamID。
  3. 客户端成功发送play请求后,服务端向客户端发送onStatus命令消息NetStream.Play.Start 和 NetStream.Play.Reset消息。其中NetStream.Play.Reset消息只有在客户端发送play消息的时候设置了reset标志的时候才会发。如果客户端请求播放的流不存在,服务端会返回onStatus命令消息NetStream.Play.StreamNotFound。
  4. 这些交互结束之后,服务端就会向客户端发送音频和视频数据,客户端就可以进行解码,然后渲染播放了。

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解析的主要内容,如果未能解决你的问题,请参考以下文章

流媒体RTMP协议解析

RTMP协议

视频接入网关是什么?RTSP转RTMP作用

RTMP解析

音频和视频流最佳选择?SRT 协议解析及报文识别

高稳定低延时高并发RTMP播放器流媒体音视频播放器EasyPlayer-RTMP-iOS器如何将核心代码打包成静态库