深入理解rtmp(四)之协议实现分析
前面的三篇文章已经从开发环境搭建实现到了握手协议:
- 深入理解rtmp(一)之开发环境搭建
- 深入理解rtmp(二)之C++脚手架搭建
- 深入理解rtmp(三)之手把手实现握手协议
根据我们第二篇深入理解rtmp(二)之C++脚手架搭建中接口封装流程:
接下来要实现connect,createStream,play,..,为了效率和篇幅,我们直接分析srs-librtmp的实现.srs-librtmp是从srs作者为srs服务实现的客户端库(The client library srs-librtmp of SRS),其实不止客户端,也实现了一个简单的server功能.
为了方便我们后续的分析,先了解两个概念:
1.准备知识
1.1大小端
举个🌰说明:
假设某段内存中存放以下这样的数据
低地址 高地址
------------------------------------>
+--------+--------+--------+--------+
| 11 | 66 | 85 | 27 |
+--------+--------+--------+--------+
- 大端模式下值为 0x11668527,数据高位存放在低地址中,数据低位存放高地址中
- 小端模式下值为 0x27856611,数据高位存放在高地址中,数据低位存放低地址中
所以在对数据处理的时候需要注意当前是在什么模式下,一般32位环境是小端模式,64位环境是大端模式.RTMP都是大端模式,所以发送数据,包头,交互消息都要填写大端模式的,但是只有streamID是小端模式
1.2AMF
Action Message Format: A compact binary format that is used to serialize ActionScript object graphs.
有两个版本:AMF0和AMF3.AMF3用作Flash Playe 9的ActionScript 3.0的默认序列化格式,而AMF0则用作旧版的ActionScript 1.0和2.0的序列化格式。 在网络传输数据方面,AMF3比AMF0更有效率。AMF3能将int和uint对象作为整数(integer)传输,并且能序列化ActionScript 3.0才支持的数据类型, 比如ByteArray,XML和Iexternalizable。
对应类型有:
类型 | 枚举值 | 说明 |
---|---|---|
Number | 0×00 | double类型 |
Boolean | 0×01 | |
String | 0×02 | string类型 |
Object | 0×03 | object类型 |
MovieClip | 0×04 | Not available in Remoting |
Null | 0×05 | null类型,空 |
Undefined | 0×06 | |
Reference | 0×07 | |
MixedArray | 0×08 | |
EndOfObject | 0×09 | See Object ,表示object结束 |
Array | 0x0a | |
Date | 0x0b | |
LongString | 0x0c | |
Unsupported | 0x0d | |
Recordset | 0x0e | Remoting, server-to-client only |
XML | 0x0f | |
TypedObject (Class instance) | 0×10 | |
AMF3 data | 0×11 | Sent by Flash player 9+ |
rtmp协议中数据都是大端的,所以在放数据前都要将数据转成大端的形式。
1.2.1 number类型
其实就是double,占8bytes。
比如:00 00 00 00 00 00 00 00,第一个byte为amf类型,其后8bytes为double值0.0。
1.2.2 boolean
就是布尔类型,占用1byte
比如:01 00,第一个byte为amf类型,其后1byte是值,false。
1.2.3 string
就是字符类型,一个byte的amf类型,两个bytes的字符长度,和N个bytes的数据。比如:02 00 02 33 22,第一个byte为amf类型,其后两个bytes为长度,注意这里的00 02是大端模式,33 22是字符数据。
1.2.4 null
是空,只有一个byte,0x05。
1.2.5 object类型
object要复杂点,第一个byte是03表示object,其后跟的是N个(key+value)。最后以00 00 09表示object结束。
- key是一个字符串组成:2bytes的长度,N bytes的数据,就是表示value的作用,相当于value的名字。
- value可以使amf任意一种类型,包括object。格式和单独的amf type一样,如果是object的话,相当于在里面再嵌套一个object。
- level(key)后面的status就是value,此value是string类型,所以格式是上面提到的string类型(0x02)。
1.2.6 ECMA_ARRAY(0x08)
实际上和object差不多,只是在0x08类型后面多了4个bytes的记录总共多少items的东西,目测全部填00也可以,也是以00 00 09结束。
0x11类型是amf3的类型,amf3实际上外层是封了一层amf0,为了与amf0兼容
1.3 rtmp url 说明
下面四种都是合法的rtmp url:
- url_schema_normal: rtmp://vhost:port/app/stream, the vhost put in host field, using DNS to resolve the server ip.
- url_schema_via : rtmp://ip:port/vhost/app/stream, VIA(vhost in app), the vhost put in app field.
- url_schema_vis : rtmp://ip:port/app/stream?vhost=xxx, VIS(vhost in stream), the vhost put in query string, keyword use vhost=xxx.
- url_schema_vis2 : rtmp://ip:port/app/stream?domain=xxx, keyword use domain=xxx.
在进行握手前,要对rtmp url进行解析,解析出host, vhost,app, stream,param等字段.
我们之前深入理解rtmp(三)之手把手实现握手协议介绍到,握手是发送一定"格式"的固定字节,我们接下来的connect,createStream以及音视频数据的内容也都是要有"格式"的,这个"格式"我们在RTMP里面是以"消息"出现的.
2.RTMP消息
我们先看一下rtmp消息格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Type | Payload length |
| (1 byte) | (3 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp |
| (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID |
| (3 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Message Header
其中:
- 1字节消息类型
- 3字节负载消息长度
- 4字节时间戳
- 3字节Stream ID,区分消息流
Message Header第一个字节Message Type(消息类型)很重要,它代表了这个消息是什么类型,当写程序的时候需要根据不同的消息,做不同的处理.我们接下来先了解下RTMP有哪些消息类型.
2.1 RTMP消息分类
消息类型Message Type下面都简写为MT
2.1.1 协议控制消息
协议控制消息是用来与对端协调控制的
MT的范围1~7:
- 1~2 用于chunk协议
- 3~6 用于rtmp协议本身,协议控制消息必须要求
Message Stream ID=0 和 Chunk Stream ID=2
- MT=1, Set Chunk Size 设置块的大小,通知对端用使用新的块大小,共4 bytes。默认大小是128字节
- MT=2, Abort Message 取消消息,用于通知正在等待接收块以完成消息的对等端,丢弃一个块流中已经接收的部分并且取消对该消息的处理,共4 bytes。
- MT=3, Acknowledgement 确认消息,客户端或服务端在接收到数量与窗口大小相等的字节后发送确认消息到对方。窗口大小是在没有接收到接收者发送的确认消息之前发送的字节数的最大值。服务端在建立连接之后发送窗口大小。本消息指定序列号。序列号,是到当前时间为止已经接收到的字节数。共4 bytes。
- MT=4, User Control Message 用户控制消息,客户端或服务端发送本消息通知对方用户的控制事件。本消息承载事件类型和事件数据。消息数据的头两个字节用于标识事件类型。事件类型之后是事件数据。事件数据字段是可变长的。
- MT=5, Window Acknowledgement Size 确认窗口大小,客户端或服务端发送本消息来通知对方发送确认消息的窗口大小,共4 bytes.
- MT=6, Set Peer Bandwidth 设置对等端带宽,客户端或服务端发送本消息更新对等端的输出带宽。发送者可以在限制类型字段(1 bytes)把消息标记为硬(0),软(1),或者动态(2)。如果是硬限制对等端必须按提供的带宽发送数据。如果是软限制,对等端可以灵活决定带宽,发送端可以限制带宽?。如果是动态限制,带宽既可以是硬限制也可以是软限制。
2.1.2 音频数据消息
- MT=8, Audio message, 客户端或服务端发送本消息用于发送音频数据。消息类型 8 ,保留为音频消息
2.1.3 视频数据消息
- MT=9, Video message, 客户端或服务端使用本消息向对方发送视频数据。消息类型值 9 ,保留为视频消息。
2.1.4 元数据消息
- MT=15或18, Data message, 客户端或服务端通过本消息向对方发送元数据和用户数据。元数据包括数据的创建时间、时长、主题等细节。消息类型为 18 的用 AMF0 编码,消息类型为 15 的用AMF3 编码。
2.1.5 共享对象消息
- MT=16或19, Shared object message, 共享对象是跨多个客户端,实例同步的 FLASH 对象(名值对的集合)。
2.1.6 命令消息
- MT=17或20, Command message, 命令消息都是用AMF编码的,AMF有两种,为AMF0和AMF3。命令消息有命令名,传输ID,和命名对象组成。而命名对象是由一系列参数组成的。
命令消息的类型有:
2.1.6.1 NetConnection Commands(连接层的命令)
代表服务端和客户端之间连接的更高层的对象。包含4个命令类型。
- connect:该命令是client先发送给server,意思是我要连接,能建立连接吗?server返回含“_result”或者“_error”命令名, 返回“_result”,表示server能提供服务,client可以进行下一步。“_error”,很明显server端不能提供服务。
- call:NetConnection 对象的调用方法在接收端运行远程过程调用。远程方法的名作为调用命令的参数。
- close:不知道为何协议里没写这个命令的内容,我猜应该是close connect。
- createStream:客户端发送本命令到服务端创建一个消息通讯的逻辑通道。音频,视频和元数据的发布是由创建流命令建立的流通道承载的。
RTMP握手之后先发送一个connect 命令消息,命令里面包含什么东西,协议中没有具体规定,实际通信中要指定一些编解码的信息,并以AMF格式发送, 下面是用wireshake抓取connect命令需要包含的参数信息:
这些信息协议中并没有特别详细说明, 在librtmp,srs-librtmp这些源码中,以及用wireshark 抓包的时候可以看到。
服务器返回的是一个_result命令类型消息,这个消息的payload length一般不会大于128字节,但是在最新的nginx-rtmp中返回的消息长度会大于128字节。
消息的transactionID是用来标识command类型的消息的,服务器返回的_result消息可以通过 transactionID来区分是对哪个命令的回应,connect 命令发完之后还要发送其他命令消息,要保证他们的transactionID不相同。
发送完connect命令之后一般会发一个 set chunk size消息来设置chunk size 的大小,也可以不发。
Window Acknowledgement Size 是设置接收端消息窗口大小,一般是2500000字节,即告诉对端在收到设置的窗口大小长度的数据之后要返回一个ACK消息。在实际做推流的时候推流端要接收很少的服务器数据,远远到达不了窗口大小,所以这个消息可以不发。而对于服务器返回的ACK消息一般也不做处理,默认服务器都已经收到了所有消息了。
之后要等待服务器对于connect消息的回应的,一般是把服务器返回的chunk都读完,组包成完整的RTMP消息,没有错误就可以进行下一步了。
connect消息结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名字) | String | 命令的名字,如”connect” |
TransactionID(事务ID) | Number | 恒为1 |
CommandObject(命令包含的参数对象) | Object | 键值对集合表示的命令参数 |
OptionalUserArguments(额外的用户参数) | Object | 用户自定义的额外信息 |
call消息结构如下:
字段 | 类型 | 说明 |
---|---|---|
ProcedureName(进程名) | String | 要调用的进程名称 |
TransactionID | Number | 如果想要对端响应的话置为非0值,否则置为0 |
CommandObject | Object | 命令参数 |
OptionalArguents | Object | 用户自定义参数 |
call消息中的TransactionID不为0的话,对端需要对该命令做出响应,响应的消息结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | 命令的名称 |
TransactionID | Number | 上面接收到的命令消息中的TransactionID |
CommandObject | Object | 命令参数 |
OptionalArguents | Object | 用户自定义参数 |
创建完RTMP连接之后就可以创建RTMP流,客户端要想服务器发送一个releaseStream命令消息,之后是FCPublish命令消息,在之后是createStream命令消息。当发送完createStream消息之后,解析服务器返回的消息会得到一个stream ID。这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
createStream消息结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “createStream” |
TransactionID | Number | 上面接收到的命令消息中的TransactionID |
CommandObject | Object | 命令参数 |
OptionalArguents | Object | 用户自定义参数 |
NetConnection 本身是默认的流通道,具有流ID 0。协议和一少部分命令消息,包括创建流,就使用默认的通讯通道。
2.1.6.2 NetStream Commands(流连接上的命令)
Netstream建立在NetConnection之上,通过NetConnection的createStream命令创建,用于传输具体的音频、视频等信息。
在传输层协议之上只能连接一个NetConnection,但一个NetConnection可以建立多个NetStream来建立不同的流通道传输数据。
以下会列出一些常用的NetStream Commands,服务端收到命令后会通过onStatus的命令来响应客户端,表示当前NetStream的状态。
onStatus命令的消息结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “onStatus” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
InfoObject | Object | AMF类型的Object,至少包含以下三个属性:1、“level”,String类型,可以为“warning”、”status”、”error”中的一种;2、”code”,String类型,代表具体状态的关键字,比如”NetStream.Play.Start”表示开始播流;3、”description”,String类型,代表对当前状态的描述,提供对当前状态可读性更好的解释,除了这三种必要信息,用户还可以自己增加自定义的键值对 |
NetStream Commands1: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
- 客户端从服务端接收到流创建成功消息,发送播放命令到服务端。
- 接收到播放命令后,服务端发送协议消息设置块大小。
- 服务端发送另一个协议消息(用户控制消息),并且在消息中指定事件” streamisrecorded” 和流 ID 。消息承载的头 2 个字,为事件类型,后4 个字节为流 ID 。
- 服务端发送事件” streambegin” 的协议消息(用户控制),告知客户端流 ID 。
- 服务端发送响应状态命令消息
NetStream.Play.Start
&NetStream.Play.reset
, 如果客户端发送的播放命令成功的话。只有当客户端发送的播放命令设置了reset
命令的条件下,服务端才发送NetStream.Play.reset
消息。如果要发送的流 没有找的话,服务端发送NetStream.Play.StreamNotFound
消息。在此之后服务端发送客户端要播放的音频和视频数据。
play命令的结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “play” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 不需要此字段,设为空 |
StreamName | String | 要播放的流的名称 |
开始位置 | Number | 可选参数,表示从何时开始播流,以秒为单位。默认为-2,代表选取对应该流名称的直播流,即当前正在推送的流开始播放,如果对应该名称的直播流不存在,就选取该名称的流的录播版本,如果这也没有,当前播流端要等待直到对端开始该名称的流的直播。如果传值-1,那么只会选取直播流进行播放,即使有录播流也不会播放;如果传值或者正数,就代表从该流的该时间点开始播放,如果流不存在的话就会自动播放播放列表中的下一个流 |
周期 | Number | 可选参数,表示回退的最小间隔单位,以秒为单位计数。默认值为-1,代表直到直播流不再可用或者录播流停止后才能回退播放;如果传值为0,代表从当前帧开始播放 |
重置 | Boolean | 可选参数,true代表清除之前的流,重新开始一路播放,false代表保留原来的流,向本地的播放列表中再添加一条播放流 |
NetStream Commands2:play2(播放)
和播放命令不同,play2命令可以切换到不同的码率,而不用改变已经播放的内容的时间线。
服务端对播放 2 命令可以请求的多个码率维护多个文件。
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “play2” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
parameters | Object | AMF编码的Flash对象,包括了一些用于描述flash.net.NetstreamPlayOptions ActionScript obejct的参数 |
NetStream Commands3:deleteStream(删除流)
当 NetStream 对象销毁的时候发送删除流命令。
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “deleteStream” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
StreamID(流ID) | Number | 本地已删除,不再需要服务器传输的流的ID |
NetStream Commands4:closeStream
NetStream Commands5:receiveAudio(接收音频)
NetStream 对象发送接收音频消息通知服务端发送还是不发送音频到客户端。
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “receiveAudio” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
BoolFlag | Boolean | true表示发送音频,如果该值为false,服务器端不做响应,如果为true的话,服务器端就会准备接受音频数据,会向客户端回复NetStream.Seek.Notify和NetStream.Play.Start的Onstatus命令告知客户端当前流的状态 |
NetStream Commands6:receiveVideo(接收视频)
NetStream 对象发送 receiveVideo 消息通知服务端是否发送视频到客户端。
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “receiveVideo” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
BoolFlag | Boolean | true表示发送视频,如果该值为false,服务器端不做响应,如果为true的话,服务器端就会准备接受视频数据,会向客户端回复NetStream.Seek.Notify和NetStream.Play.Start的Onstatus命令告知客户端当前流的状态 |
NetStream Commands7:publish(推送数据)
推流准备工作的最后一步是 Publish Stream,即向服务器发一个publish命令消息,这个命令的message stream ID 就是上面 create stream 之后服务器返回的stream ID,发完这个命令一般不用等待服务器返回的回应,直接发送音视频类型的RTMP数据包即可。有些rtmp库还会发setMetaData消息,这个消息可以发也可以不发,里面包含了一些音视频meta data的信息,如视频的分辨率等等。
+-------------+ +----------+
| Client | | | Server |
+-------------+ | +----------+
| | Handshaking Done | |
| | |
| | |
---+---- |--------- Command Message(connect) --------->|
| | |
Connect |<---------- Window Acknowledge Size -------------|
| | |
| |<------------- Set Peer BandWidth ---------------|
| | |
| |----------- Window Acknowledge Size ------------>|
| | |
| |<--------- User Control(StreamBegin) ------------|
| | |
---+---- |--------------- Command Message ---------------->|
| (_result- connect response) |
| |
---+---- |---------Command Message(createStream) --------->|
Create | |
Stream | |
---+---- |<-------------- Command Message -----------------|
| (_result- createStream response) |
| |
---+---- |--------- Command Message (publish) ------------>|
| | |
publish |<-------- UserControl (StreamBegin) -------------|
| | |
| |---------- Data Message (Metadata) ------------->|
| | |
| |------------------ Audio Message---------------->|
| | |
| |----------------- SetChunkSize ----------------->|
| | |
| |<--------------- Command Message ----------------|
| | (_result- publish result) |
| |------------------ Video Message---------------->|
|
|
Until the stream is complete
publish命令结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “publish” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
PublishingName(推流的名称) | String | 流名称 |
PublishingType(推流类型) | String | “live”、”record”、”append”中的一种。live表示该推流文件不会在服务器端存储;record表示该推流的文件会在服务器应用程序下的子目录下保存以便后续播放,如果文件已经存在的话删除原来所有的内容重新写入;append也会将推流数据保存在服务器端,如果文件不存在的话就会建立一个新文件写入,如果对应该流的文件已经存在的话保存原来的数据,在文件末尾接着写入 |
NetStream Commands8:seek(定位流的位置)
定位到视频或音频的某个位置,以毫秒为单位。
客户端发送搜寻命令在一个媒体文件中或播放列表中搜寻偏移。
seek命令的结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “seek” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
milliSeconds | Number | 定位到该文件的xx毫秒处 |
NetStream Commands9:pause(暂停)
客户端告知服务端停止或恢复播放, 客户端发送暂停命令告诉服务端暂停或开始一个命令。
pause命令的结构如下:
字段 | 类型 | 说明 |
---|---|---|
CommandName(命令名) | String | “pause” |
TransactionID | Number | 恒为0 |
CommandObject | NULL | 对onSatus命令来说不需要这个字段 |
Pause/Unpause Flag | Boolean | true表示暂停,false表示恢复 |
milliSeconds | Number | 暂停或者恢复的时间,以毫秒为单位 |
2.1.7 聚合消息
MT=22, Aggregate message, 聚合消息是含有一个消息列表的一种消息。消息类型值 22 ,保留用于聚合消息。
+---------+-------------------------+ | Header | Aggregate Message body | +---------+-------------------------+ 聚合消息的格式 +--------+--------------+--------------+--------+-------------+---------------+ - - - - |Header 0|Message Data 0|Back Pointer 0|Header 1|Message Data 1|Back Pointer 1| +--------+--------------+--------------+--------+--------------+--------------+ - - - - 聚合消息的body
Back Pointer包含了前面消息的大小(包括Header的大小)。这个设置匹配了 flv 文件格式,可用于后向搜索。
2.2 接收命令消息反馈结果 ResponseCommand
通过块消息携带的数据,拼接成消息内容,通过AMF解读消息内容
#define RTMPConnectSuccess @"NetConnection.Connect.Success"
#define RTMPPublishStart @"NetStream.Publish.Start"
#define RTMPPublishBadName @"NetStream.Publish.BadName"
#define RTMPPlayStart @"NetStream.Play.Start"
#define RTMPPlayReset @"NetStream.Play.Reset"
#define RTMPPlayStreamNotFound @"NetStream.Play.StreamNotFound"
typedef enum : char {
RTMPResponseCommand_Result = 0x1, //_Result命令
RTMPResponseCommandOnBWDone = 0x2, //OnBWDone命令
RTMPResponseCommandOnFCPublish = 0x3, //OnFCPublish命令
RTMPResponseCommandOnStatus = 0x4, //OnStatus命令
RTMPResponseCommandOnFCUnpublish = 0x5, //OnFCUnpublish命令
RTMPResponseCommandOnMetaData = 0x6, //OnMetaData命令
RTMPResponseCommandUnkonwn = 0x7f,//未知类型
} RTMPResponseCommandType;
上面分析完了rtmp消息类型,但实际RTMP通信中并未按照上述格式去发送RTMP消息,而是将RTMP 消息分块发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接收端也会按照这个id来将chunk组装成Message。为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。
下面了解下RTMP消息分块
2.3 RTMP消息分块
每个Chunk都由 <font color=red>Chunk Header + Chunk Data </font>组成:
+-------+ +--------------+----------------+
| Chunk | = | Chunk Header | Chunk Data |
+-------+ +--------------+----------------+
2.3.1 Chunk Header
Chunk Header 由 Basic Header + Message Header + ExtendedTimestamp(不一定存在)组成:
+--------------+ +-------------+----------------+-------------------+
| Chunk Header | = | Basic header| Message Header |Extended Timestamp |
+--------------+ +-------------+----------------+-------------------+
2.3.1.1 Basic Header(基本的头信息) (1~3 byte)
+-+-+-+-+-+-+-+-+
|fmt| cs id |
+-+-+-+-+-+-+-+-+
<font color=red>chuck stream = cs</font>
- fmt: 表示块类型,决定了Chunk Msg Header的格式,它占第一个字节的0~1bit两位,
csid:表示块流id 占2~7bit属于csid。
①csid在64~319的范围内时,
csidTS=0,csid =(第二个字节的值)+640 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |fmt|0 0 0 0 0 0|the second byte| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
②csid在64~65599的范围内时(①与②存在交集,原则上交集部分选择①),
csidTS=0x3f,bit位全为1时,csid=(第二个字节的值×256)+(第三个字节的值)+640 1 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |fmt|1 1 1 1 1 1|the second byte| the third byte| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
③csid在3~63的范围内时,
csidTS=1~0x3e,即6位bit非全0也非全1时,csid=csidTS0 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |fmt|0<csid<0x3f| +-+-+-+-+-+-+-+-+
注意:这里第一个字节的2~7bit的值暂时称之为csidTS 由此可见:Basic Header的长度范围为1~3 byte,具体多少byte是csidTS决定的,csid的值范围3~65599,0~2作为保留。
2.3.1.2 Chuck Message Header(块消息的消息头信息)(0、3、7、11 byte)
包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt字段控制。
其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。以下按照字节数从多到少的顺序分别介绍这4种格式的Chuck Message Header
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp | message length:
+-------------------------------+---------------+---------------+
: |message type id| :
+-------------------------------+---------------+---------------+
: message stream id |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- timestamp(时间戳): 占 3 byte 最大表示16777215=0xFFFFFF=2^24-1,超出这个值,这3个字节置为1,将实际数据转存到Extended Timestamp字段中。
- message length(长度): 占 3 byte 表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。
- message type id(消息的类型id): 占 1 byte 表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。
- message stream id(消息的流id): 占 4byte 表示该chunk所在的流的ID,和Basic Header的CSID一样,它采用小端存储的方式。
- fmt=0:长度11 byte,其他三种能表示的数据它都能表示 , 在一个块流的开始和时间戳返回的时候必须有这种块: Chuck Message Header = timestamp + message length + message type id + message stream id
- fmt=1:长度为7 byte,与fmt=0时,相比,该类型少了Message Stream Id,具有可变大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式:Chuck Message Header = timestamp + message length + message type id
- fmt=2:长度3 byte,不包含Message Stream Id和Message Length 、Message type Id,具有固定大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式:Chuck Message Header = timestamp
- fmt=3:长度为0 byte,当一个消息被分成多个块,除了第一块以外,所有的块都应使用这种类型 : Chuck Message Header = 0 byte
注意:
message type id 发送音视频数据的时候
- 如果包头MessageTypeID为0x8或0x9,数据(chunk data)是flv的tag data(没有tag header),flv格式封装请见FLV格式解析
- 也可以用新类型MessageTypeID为0x16,数据(chunk data)是一个完整flv的tag(tag header + tag data)
- message stream id 采用<font color=red>小端</font>存储
- RTMP都是大端模式,所以发送数据,包头,交互消息都要填写大端模式的,但是只有streamID是小端模式
2.3.1.3 ExtendedTimestamp(扩展时间) (0、4 byte)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 只有当块消息头中的普通时间戳设置为 0xffffff 时,本字段才被传送。
- 如果普通时间戳的值小于 0x00ffffff ,那么本字段一定不能出现。
- 如果块消息头中时间戳字段不出现本字段也一定不能出现。
类型 3 的块一定不能含有本字段。
- 本字段在块消息头之后,块数据之前。
2.3.2 Chunk Data
+-----------+
|Chunk Data |
+-----------+
用户层面上真正想要发送的与协议无关的数据,长度在(0,chunkSize]之间, chunk size默认为128字节。
2.3.3 RTMP消息分块注意事项
- Chunk Size: RTMP是按照chunk size进行分块,chunk size 指的是 chunk的payload部分的大小,不包括chunk basic header 和 chunk message header长度。客户端和服务器端各自维护了两个chunk size, 分别是自身分块的chunk size 和 对端 的chunk size, 默认的这两个chunk size都是128字节。通过向对端发送set chunk size 消息可以告知对方更改了 chunk size的大小。
- Chunk Type: RTMP消息分成的Chunk有4种类型,可以通过 chunk basic header的高两位(fmt)指定,一般在拆包的时候会把一个RTMP消息拆成以格式0开始的chunk,之后的包拆成格式3 类型的chunk,我查看了有不少代码也是这样实现的,这样也是最简单的实现。
如果第二个message和第一个message的message stream ID 相同,并且第二个message的长度也大于了chunk size,那么该如何拆包?当时查了很多资料,都没有介绍。后来看了一些源码,如 SRS,FFMPEG中的实现,发现第二个message可以拆成Type_1类型一个chunk, message剩余的部分拆成Type_3类型的chunk。FFMPEG中就是这么做的。
srs-librtmp分块结构封装:
class SrsChunkStream
{
public:
// Represents the basic header fmt,
// which used to identify the variant message header type.
char fmt;
// Represents the basic header cid,
// which is the chunk stream id.
int cid;
// Cached message header
SrsMessageHeader header;
// Whether the chunk message header has extended timestamp.
bool extended_timestamp;
// The partially read message.
SrsCommonMessage* msg;
// Decoded msg count, to identify whether the chunk stream is fresh.
int64_t msg_count;
public:
SrsChunkStream(int _cid);
virtual ~SrsChunkStream();
};
3.代码分析
我们先看几个srs-librtmp封装的结构:
runtime context, 我们初始化配置及传入的url以及url解析结果等:
struct Context
{
// The original RTMP url.
std::string url;
// Parse from url.
std::string tcUrl;
std::string host;
std::string vhost;
std::string app;
std::string stream;
std::string param;
// Parse ip:port from host.
std::string ip;
int port;
// The URL schema, about vhost/app/stream?param
srs_url_schema schema;
// The server information, response by connect app.
SrsServerInfo si;
// The extra request object for connect to server, NULL to ignore.
SrsRequest* req;
// the message received cache,
// for example, when got aggregate message,
// the context will parse to videos/audios,
// and return one by one.
std::vector<SrsCommonMessage*> msgs;
SrsRtmpClient* rtmp;
SimpleSocketStream* skt;
int stream_id;
// the remux raw codec.
SrsRawH264Stream avc_raw;
SrsRawAacStream aac_raw;
// about SPS, @see: 7.3.2.1.1, ISO_IEC_14496-10-AVC-2012.pdf, page 62
std::string h264_sps;
std::string h264_pps;
// whether the sps and pps sent,
// @see https://github.com/ossrs/srs/issues/203
bool h264_sps_pps_sent;
// only send the ssp and pps when both changed.
// @see https://github.com/ossrs/srs/issues/204
bool h264_sps_changed;
bool h264_pps_changed;
// the aac sequence header.
std::string aac_specific_config;
// user set timeout, in ms.
int64_t stimeout;
int64_t rtimeout;
// The RTMP handler level buffer, can used to format packet.
char buffer[1024];
Context() : port(0) {
rtmp = NULL;
skt = NULL;
req = NULL;
stream_id = 0;
h264_sps_pps_sent = false;
h264_sps_changed = false;
h264_pps_changed = false;
rtimeout = stimeout = SRS_UTIME_NO_TIMEOUT;
schema = srs_url_schema_normal;
}
virtual ~Context() {
srs_freep(req);
srs_freep(rtmp);
srs_freep(skt);
std::vector<SrsCommonMessage*>::iterator it;
for (it = msgs.begin(); it != msgs.end(); ++it) {
SrsCommonMessage* msg = *it;
srs_freep(msg);
}
msgs.clear();
}
};
里面又涉及到了四个重要结构:
- SrsServerInfo:The server information, response by connect app.
- SrsRequest:The extra request object for connect to server, NULL to ignore.
- SrsRtmpClient:rtmp客户端相关信息
- SimpleSocketStream: 真正的socket封装,跟我们之前文章深入理解rtmp(二)之脚手架搭建中的SimpleSocketStream一样
SrsServerInfo结构,里面信息对于我们客户端用处不是特别大:
struct SrsServerInfo
{
std::string ip;
std::string sig;
int pid;
int cid;
int major;
int minor;
int revision;
int build;
SrsServerInfo();
};
SrsRequest结构:
class SrsRequest
{
public:
// The client ip.
std::string ip;
public:
// The tcUrl: rtmp://request_vhost:port/app/stream
// support pass vhost in query string, such as:
// rtmp://ip:port/app?vhost=request_vhost/stream
// rtmp://ip:port/app...vhost...request_vhost/stream
std::string tcUrl;
std::string pageUrl;
std::string swfUrl;
double objectEncoding;
// The data discovery from request.
public:
// Discovery from tcUrl and play/publish.
std::string schema;
// The vhost in tcUrl.
std::string vhost;
// The host in tcUrl.
std::string host;
// The port in tcUrl.
int port;
// The app in tcUrl, without param.
std::string app;
// The param in tcUrl(app).
std::string param;
// The stream in play/publish
std::string stream;
// For play live stream,
// used to specified the stop when exceed the duration.
// @see https://github.com/ossrs/srs/issues/45
// in srs_utime_t.
srs_utime_t duration;
// The token in the connect request,
// used for edge traverse to origin authentication,
// @see https://github.com/ossrs/srs/issues/104
SrsAmf0Object* args;
public:
SrsRequest();
virtual ~SrsRequest();
public:
// Deep copy the request, for source to use it to support reload,
// For when initialize the source, the request is valid,
// When reload it, the request maybe invalid, so need to copy it.
virtual SrsRequest* copy();
// update the auth info of request,
// To keep the current request ptr is ok,
// For many components use the ptr of request.
virtual void update_auth(SrsRequest* req);
// Get the stream identify, vhost/app/stream.
virtual std::string get_stream_url();
// To strip url, user must strip when update the url.
virtual void strip();
public:
// Transform it as HTTP request.
virtual SrsRequest* as_http();
};
SrsRtmpClient:
// implements the client role protocol.
class SrsRtmpClient
{
private:
SrsHandshakeBytes* hs_bytes;//跟我们之前封装的握手接口类似
protected:
SrsProtocol* protocol;//rtmp协议封装,就是封装了我们的connect, createStream,publish等接口
ISrsProtocolReadWriter* io;//就是SimpleSocketStream
public:
SrsRtmpClient(ISrsProtocolReadWriter* skt);
virtual ~SrsRtmpClient();
// Protocol methods proxy
public:
virtual void set_recv_timeout(srs_utime_t tm);
virtual void set_send_timeout(srs_utime_t tm);
virtual int64_t get_recv_bytes();
virtual int64_t get_send_bytes();
virtual srs_error_t recv_message(SrsCommonMessage** pmsg);
virtual srs_error_t decode_message(SrsCommonMessage* msg, SrsPacket** ppacket);
virtual srs_error_t send_and_free_message(SrsSharedPtrMessage* msg, int stream_id);
virtual srs_error_t send_and_free_messages(SrsSharedPtrMessage** msgs, int nb_msgs, int stream_id);
virtual srs_error_t send_and_free_packet(SrsPacket* packet, int stream_id);
public:
// handshake with server, try complex, then simple handshake.
virtual srs_error_t handshake();
// only use simple handshake
virtual srs_error_t simple_handshake();
// only use complex handshake
virtual srs_error_t complex_handshake();
// Connect to RTMP tcUrl and app, get the server info.
//
// @param app, The app to connect at, for example, live.
// @param tcUrl, The tcUrl to connect at, for example, rtmp://ossrs.net/live.
// @param req, the optional req object, use the swfUrl/pageUrl if specified. NULL to ignore.
// @param dsu, Whether debug SRS upnode. For edge, set to true to send its info to upnode.
// @param si, The server information, retrieve from response of connect app request. NULL to ignore.
virtual srs_error_t connect_app(std::string app, std::string tcUrl, SrsRequest* r, bool dsu, SrsServerInfo* si);
// Create a stream, then play/publish data over this stream.
virtual srs_error_t create_stream(int& stream_id);
// start play stream.
virtual srs_error_t play(std::string stream, int stream_id, int chunk_size);
// start publish stream. use flash publish workflow:
// connect-app => create-stream => flash-publish
virtual srs_error_t publish(std::string stream, int stream_id, int chunk_size);
// start publish stream. use FMLE publish workflow:
// connect-app => FMLE publish
virtual srs_error_t fmle_publish(std::string stream, int& stream_id);
public:
template<class T>
srs_error_t expect_message(SrsCommonMessage** pmsg, T** ppacket)
{
return protocol->expect_message<T>(pmsg, ppacket);
}
};
SimpleSocketStream和我们之前的类似,只是srs作者做了一层接口抽象:
/**
* The system io reader/writer architecture:
* +---------------+ +---------------+
* | IStreamWriter | | IVectorWriter |
* +---------------+ +---------------+
* | + write() | | + writev() |
* +-------------+-+ ++--------------+
* +----------+ +--------------------+ /\ /\
* | IReader | | IStatistic | \ /
* +----------+ +--------------------+ V
* | + read() | | + get_recv_bytes() | +------+----+
* +------+---+ | + get_send_bytes() | | IWriter |
* / \ +---+--------------+-+ +-------+---+
* | / \ / \ / \
* | | | |
* +------+-------------+------+ ++---------------------+--+
* | IProtocolReader | | IProtocolWriter |
* +---------------------------+ +-------------------------+
* | + readfully() | | + set_send_timeout() |
* | + set_recv_timeout() | +-------+-----------------+
* +------------+--------------+ / \
* / \ |
* | |
* +--+-----------------------------+-+
* | IProtocolReadWriter |
* +----------------------------------+
*/
我们再看一下SrsProtocol:
// The protocol provides the rtmp-message-protocol services,
// To recv RTMP message from RTMP chunk stream,
// and to send out RTMP message over RTMP chunk stream.
class SrsProtocol
{
private:
class AckWindowSize
{
public:
uint32_t window;
// number of received bytes.
int64_t nb_recv_bytes;
// previous responsed sequence number.
uint32_t sequence_number;
AckWindowSize();
};
// For peer in/out
private:
// The underlayer socket object, send/recv bytes.
ISrsProtocolReadWriter* skt;
// The requests sent out, used to build the response.
// key: transactionId
// value: the request command name
std::map<double, std::string> requests;
// For peer in
private:
// The chunk stream to decode RTMP messages.
std::map<int, SrsChunkStream*> chunk_streams;
// Cache some frequently used chunk header.
// cs_cache, the chunk stream cache.
SrsChunkStream** cs_cache;
// The bytes buffer cache, recv from skt, provide services for stream.
SrsFastStream* in_buffer;
// The input chunk size, default to 128, set by peer packet.
int32_t in_chunk_size;
// The input ack window, to response acknowledge to peer,
// For example, to respose the encoder, for server got lots of packets.
AckWindowSize in_ack_size;
// The output ack window, to require peer to response the ack.
AckWindowSize out_ack_size;
// The buffer length set by peer.
int32_t in_buffer_length;
// Whether print the protocol level debug info.
// Generally we print the debug info when got or send first A/V packet.
bool show_debug_info;
// Whether auto response when recv messages.
// default to true for it's very easy to use the protocol stack.
bool auto_response_when_recv;
// When not auto response message, manual flush the messages in queue.
std::vector<SrsPacket*> manual_response_queue;
// For peer out
private:
iovec* out_iovs;
int nb_out_iovs;
char* out_c0c3_caches;
// Whether warned user to increase the c0c3 header cache.
bool warned_c0c3_cache_dry;
// The output chunk size, default to 128, set by config.
int32_t out_chunk_size;
public:
SrsProtocol(ISrsProtocolReadWriter* io);
virtual ~SrsProtocol();
public:
// Set the auto response message when recv for protocol stack.
virtual void set_auto_response(bool v);
virtual srs_error_t manual_response_flush();
public:
public:
virtual void set_recv_timeout(srs_utime_t tm);
virtual srs_utime_t get_recv_timeout();
virtual void set_send_timeout(srs_utime_t tm);
virtual srs_utime_t get_send_timeout();
// Get recv/send bytes.
virtual int64_t get_recv_bytes();
virtual int64_t get_send_bytes();
public:
virtual srs_error_t set_in_window_ack_size(int ack_size);
public:
virtual srs_error_t recv_message(SrsCommonMessage** pmsg);
virtual srs_error_t decode_message(SrsCommonMessage* msg, SrsPacket** ppacket);
virtual srs_error_t send_and_free_message(SrsSharedPtrMessage* msg, int stream_id);
virtual srs_error_t send_and_free_messages(SrsSharedPtrMessage** msgs, int nb_msgs, int stream_id);
virtual srs_error_t send_and_free_packet(SrsPacket* packet, int stream_id);
private:
virtual srs_error_t do_send_messages(SrsSharedPtrMessage** msgs, int nb_msgs);
virtual srs_error_t do_iovs_send(iovec* iovs, int size);
virtual srs_error_t do_send_and_free_packet(SrsPacket* packet, int stream_id);
virtual srs_error_t do_decode_message(SrsMessageHeader& header, SrsBuffer* stream, SrsPacket** ppacket);
virtual srs_error_t recv_interlaced_message(SrsCommonMessage** pmsg);
virtual srs_error_t read_basic_header(char& fmt, int& cid);
virtual srs_error_t read_message_header(SrsChunkStream* chunk, char fmt);
virtual srs_error_t read_message_payload(SrsChunkStream* chunk, SrsCommonMessage** pmsg);
virtual srs_error_t on_recv_message(SrsCommonMessage* msg);
virtual srs_error_t on_send_packet(SrsMessageHeader* mh, SrsPacket* packet);
private:
virtual srs_error_t response_acknowledgement_message();
virtual srs_error_t response_ping_message(int32_t timestamp);
private:
virtual void print_debug_info();
};
了解完主要的结构,继续看connect app:
connect
最终调用到了SrsRtmpClient里面的connect_app:
context->rtmp->connect_app(c->app, tcUrl, c->req, true, &c->si))
tcUrl格式:rtmp://host或vhost:port/app
,req可以调用srs_rtmp_set_connect_args创建,默认为空,&c->si
是要存储服务端返回的SrsServerInfo
在connect_app中首先构造了SrsConnectAppPacket:
SrsConnectAppPacket* pkt = new SrsConnectAppPacket();
pkt->command_object->set("app", SrsAmf0Any::str(app.c_str()));
pkt->command_object->set("flashVer", SrsAmf0Any::str("WIN 15,0,0,239"));
if (r) {
pkt->command_object->set("swfUrl", SrsAmf0Any::str(r->swfUrl.c_str()));
} else {
pkt->command_object->set("swfUrl", SrsAmf0Any::str());
}
if (r && r->tcUrl != "") {
pkt->command_object->set("tcUrl", SrsAmf0Any::str(r->tcUrl.c_str()));
} else {
pkt->command_object->set("tcUrl", SrsAmf0Any::str(tcUrl.c_str()));
}
pkt->command_object->set("fpad", SrsAmf0Any::boolean(false));
pkt->command_object->set("capabilities", SrsAmf0Any::number(239));
pkt->command_object->set("audioCodecs", SrsAmf0Any::number(3575));
pkt->command_object->set("videoCodecs", SrsAmf0Any::number(252));
pkt->command_object->set("videoFunction", SrsAmf0Any::number(1));
if (r) {
pkt->command_object->set("pageUrl", SrsAmf0Any::str(r->pageUrl.c_str()));
} else {
pkt->command_object->set("pageUrl", SrsAmf0Any::str());
}
pkt->command_object->set("objectEncoding", SrsAmf0Any::number(0));
if (dsu && r && r->args) {
srs_freep(pkt->args);
pkt->args = r->args->copy()->to_object();
}
if ((err = protocol->send_and_free_packet(pkt, 0)) != srs_success) {//发送packet
return srs_error_wrap(err, "send packet");
}
SrsConnectAppPacket结构:
class SrsConnectAppPacket : public SrsPacket
{
public:
// Name of the command. Set to "connect".
std::string command_name;
// Always set to 1.
double transaction_id;
SrsAmf0Object* command_object;
SrsAmf0Object* args;
public:
SrsConnectAppPacket();
virtual ~SrsConnectAppPacket();
// Decode functions for concrete packet to override.
public:
virtual srs_error_t decode(SrsBuffer* stream);
// Encode functions for concrete packet to override.
public:
virtual int get_prefer_cid();
virtual int get_message_type();
protected:
virtual int get_size();
virtual srs_error_t encode_packet(SrsBuffer* stream);
};
对应我们前面connect消息结构封装,command_name固定为"connect",transaction_id固定为1
发送packet调用SrsProtocol的send_and_free_packet,最终调用到:
srs_error_t SrsProtocol::do_send_and_free_packet(SrsPacket* packet, int stream_id)
{
srs_error_t err = srs_success;
srs_assert(packet);
SrsAutoFree(SrsPacket, packet);
SrsCommonMessage* msg = new SrsCommonMessage();
SrsAutoFree(SrsCommonMessage, msg);
if ((err = packet->to_msg(msg, stream_id)) != srs_success) {
return srs_error_wrap(err, "to message");
}
SrsSharedPtrMessage* shared_msg = new SrsSharedPtrMessage();
if ((err = shared_msg->create(msg)) != srs_success) {
srs_freep(shared_msg);
return srs_error_wrap(err, "create message");
}
if ((err = send_and_free_message(shared_msg, stream_id)) != srs_success) {
return srs_error_wrap(err, "send packet");
}
if ((err = on_send_packet(&msg->header, packet)) != srs_success) {
return srs_error_wrap(err, "on send packet");
}
return err;
}
先将packet转换为message,主要是构造MessageHeader,最终在do_send_messages将消息拆分成块,再通过socket将块发送出去
srs_error_t SrsProtocol::do_send_messages(SrsSharedPtrMessage** msgs, int nb_msgs)
{
srs_error_t err = srs_success;
#ifdef SRS_PERF_COMPLEX_SEND
int iov_index = 0;
iovec* iovs = out_iovs + iov_index;
int c0c3_cache_index = 0;
char* c0c3_cache = out_c0c3_caches + c0c3_cache_index;
// try to send use the c0c3 header cache,
// if cache is consumed, try another loop.
for (int i = 0; i < nb_msgs; i++) {
SrsSharedPtrMessage* msg = msgs[i];
if (!msg) {
continue;
}
// ignore empty message.
if (!msg->payload || msg->size <= 0) {
continue;
}
// p set to current write position,
// it's ok when payload is NULL and size is 0.
char* p = msg->payload;
char* pend = msg->payload + msg->size;
// always write the header event payload is empty.
while (p < pend) {
// always has header
int nb_cache = SRS_CONSTS_C0C3_HEADERS_MAX - c0c3_cache_index;
int nbh = msg->chunk_header(c0c3_cache, nb_cache, p == msg->payload);
srs_assert(nbh > 0);
// header iov
iovs[0].iov_base = c0c3_cache;
iovs[0].iov_len = nbh;
// payload iov
int payload_size = srs_min(out_chunk_size, (int)(pend - p));
iovs[1].iov_base = p;
iovs[1].iov_len = payload_size;
// consume sendout bytes.
p += payload_size;
// realloc the iovs if exceed,
// for we donot know how many messges maybe to send entirely,
// we just alloc the iovs, it's ok.
if (iov_index >= nb_out_iovs - 2) {
int ov = nb_out_iovs;
nb_out_iovs = 2 * nb_out_iovs;
int realloc_size = sizeof(iovec) * nb_out_iovs;
out_iovs = (iovec*)realloc(out_iovs, realloc_size);
srs_warn("resize iovs %d => %d, max_msgs=%d", ov, nb_out_iovs, SRS_PERF_MW_MSGS);
}
// to next pair of iovs
iov_index += 2;
iovs = out_iovs + iov_index;
// to next c0c3 header cache
c0c3_cache_index += nbh;
c0c3_cache = out_c0c3_caches + c0c3_cache_index;
// the cache header should never be realloc again,
// for the ptr is set to iovs, so we just warn user to set larger
// and use another loop to send again.
int c0c3_left = SRS_CONSTS_C0C3_HEADERS_MAX - c0c3_cache_index;
if (c0c3_left < SRS_CONSTS_RTMP_MAX_FMT0_HEADER_SIZE) {
// only warn once for a connection.
if (!warned_c0c3_cache_dry) {
srs_warn("c0c3 cache header too small, recoment to %d", SRS_CONSTS_C0C3_HEADERS_MAX + SRS_CONSTS_RTMP_MAX_FMT0_HEADER_SIZE);
warned_c0c3_cache_dry = true;
}
// when c0c3 cache dry,
// sendout all messages and reset the cache, then send again.
if ((err = do_iovs_send(out_iovs, iov_index)) != srs_success) {
return srs_error_wrap(err, "send iovs");
}
// reset caches, while these cache ensure
// atleast we can sendout a chunk.
iov_index = 0;
iovs = out_iovs + iov_index;
c0c3_cache_index = 0;
c0c3_cache = out_c0c3_caches + c0c3_cache_index;
}
}
}
// maybe the iovs already sendout when c0c3 cache dry,
// so just ignore when no iovs to send.
if (iov_index <= 0) {
return err;
}
return do_iovs_send(out_iovs, iov_index);
#else
// try to send use the c0c3 header cache,
// if cache is consumed, try another loop.
for (int i = 0; i < nb_msgs; i++) {
SrsSharedPtrMessage* msg = msgs[i];
if (!msg) {
continue;
}
// ignore empty message.
if (!msg->payload || msg->size <= 0) {
continue;
}
// p set to current write position,
// it's ok when payload is NULL and size is 0.
char* p = msg->payload;
char* pend = msg->payload + msg->size;
// always write the header event payload is empty.
while (p < pend) {
// for simple send, send each chunk one by one
iovec* iovs = out_iovs;
char* c0c3_cache = out_c0c3_caches;
int nb_cache = SRS_CONSTS_C0C3_HEADERS_MAX;
// always has header
int nbh = msg->chunk_header(c0c3_cache, nb_cache, p == msg->payload);
srs_assert(nbh > 0);
// header iov
iovs[0].iov_base = c0c3_cache;
iovs[0].iov_len = nbh;
// payload iov
int payload_size = srs_min(out_chunk_size, pend - p);
iovs[1].iov_base = p;
iovs[1].iov_len = payload_size;
// consume sendout bytes.
p += payload_size;
if ((er = skt->writev(iovs, 2, NULL)) != srs_success) {
return srs_error_wrap(err, "writev");
}
}
}
return err;
#endif
}
connect后设置SrsSetWindowAckSizePacket,然后从服务端读取SrsConnectAppResPacket并解析.
贴的代码有点多,篇幅太长了,这篇先分析到这里面,后面再写续一篇代码分析,分析一下音视频收发以及srs-librtmp的一些优化.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。