阅读前置
总体逻辑
RTMP在收发数据的时候并不是以Message为单位的,⽽是把Message拆分成Chunk发送,⽽且必须在⼀个Chunk发送完成之后才能开始发送下⼀个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。Chunk的默认⼤⼩是128字节,在传输过程中,通过⼀个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最⼤值,在发送端和接受端会各⾃维护⼀个Chunk Size(srs流媒体服务器默认是60000),可以分别设置这个值来改变⾃⼰这⼀⽅发送的Chunk的最⼤⼤⼩。
在实际发送时应对要发送的数据⽤不同的Chunk Size去尝试,通过抓包分析等⼿段得出合适的Chunk⼤⼩,并且在传输过程中可以根据当前的带宽信息和实际信息的⼤⼩动态调整Chunk的⼤⼩,从⽽尽量提⾼CPU的利⽤率并减少信息的阻塞机率。
RTMP传输逻辑的关键结构
Step 1: 把数据封装成消息(Message)。
Step 2: 把消息分割成消息块(Chunk, 网络中实际传输的内容)。
Step 3: 将分割后的消息块(Chunk)通过TCP协议发送出去。接收端:
Step 1: 在通过TCP协议收到数据后, 先将消息块重新组合成消息(Message)。
Step 2: 通过对消息进行解封装处理就可以恢复出数据现在可以明白主要概念是 Message 和 Chunk,把这两个搞明白就轻车熟路了
Message
TypeId(类型Id):消息的类型Id,1个字节
Length(⻓度):是指Message Payload(消息负载)即⾳视频等信息的数据的⻓度,3个字节
Timestamp(时间戳):消息的时间戳(但不⼀定是当前时间,后⾯会介绍),4个字节
Message Stream ID(消息的流ID):每个消息的唯⼀标识,划分成Chunk和还原Chunk为Message的时候都是根据这个ID来辨识是否是同⼀个消息的Chunk的,4个字节,并且以⼩端格式存储
Message Stream ID如何产⽣?
通过createStream命令产生,然后_result返回给客户端。audio和video使⽤不同的Message Stream ID,Message StreamID是音视频流的唯一ID, 一路流如果既有音频包又有视频包,那么这路流音频包的StreamID和他视频包的StreamID相同。
Message Stream ID 指的是消息级别的 ID
Stream ID 指的是数据流级别的 ID
Message消息主要分为三类: 协议控制消息、数据消息、命令消息等
- Protocol Control Messages 用于协议层面的控制
- User Control Messages 用于应用层事件通知
- Command Messages 用于客户端服务器交互
- Data Messages 用于传输音视频等数据
命令消息
Command Message(命令消息,Message Type ID=17 或20)
此类型消息主要有NetConnection和NetStream两个类,两个类分别有多个函数,该消息的调用,可理解为远程函数调用。
实际使⽤时只是使⽤了ID=20,发送端发送时会带有命令的名字,如connect,TransactionID表示此次命 令的标识,Command Object表示相关参数。接受端收到命令后,会返回以下三种消息中的⼀种:_result 消息表示接受该命令,对端可以继续往下执⾏流程,_error消息代表拒绝该命令要执⾏的操作,method name消息代表要在之前命令的发送端执⾏的函数名称。这三种回应的消息都要带有收到的命令消息中的 TransactionId来表示本次的回应作⽤于哪个命令。 可以认为发送命令消息的对象有两种,⼀种是NetConnection,表示双端的上层连接,⼀种是 NetStream,表示流信息的传输通道,控制流信息的状态,如Play播放流,Pause暂停。
NetConnection Commands(连接层的命令)
⽤来管理双端之间的连接状态,同时也提供了异步远程⽅法调⽤(RPC)在对端执⾏某⽅法
NetConnection是connect命令之后生成的对象,通过此对象管理netstream 1:n的关系
这里大概描述命令。
connect:⽤于客户端向服务器发送连接请求
Call:⽤于在对端执⾏某函数,即常说的RPC:远程进程调⽤
Create Stream:创建传递具体信息的通道,从⽽可以在这个流中传递具体信息,传输信息单元为 Chunk。
NetStream Commands(流连接上的命令)
Netstream建⽴在NetConnection之上,通过NetConnection的createStream命令创建,⽤于传输具体的⾳频、视频等信息。在传输层协议之上只能连接⼀个NetConnection,但⼀个NetConnection可以建⽴多个NetStream来建⽴不同的流通道传输数据。这里大概描述命令:
onStatus命令服务端收到命令后会通过onStatus的命令来响应客户 端,表示当前NetStream的状态。
play(播放):
deleteStream(删除流):⽤于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此 路流。
receiveAudio(接收⾳频):通知服务器端该客户端是否要发送⾳频
receiveVideo(接收视频):通知服务器端该客户端是否要发送视频
publish(推送数据):由客户端向服务器发起请求推流到服务器。
seek(定位流的位置):定位到视频或⾳频的某个位置,以毫秒为单位。
pause(暂停):客户端告知服务端停⽌或恢复播放。
具体详细信息格式查看官方文档,比如connect:⽤于客户端向服务器发送连接请求,握⼿之后先发送⼀个connect 命令消息,这些信息是以AMF格式发送的,消息的结构如下:
AMF格式:const user = {
name: "John Doe",
age: 30,
isAdmin: true
};
⽤户控制消息
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。
协议控制消息
协议控制消息 Message Type ID = 1 2 3 5 6和Message Type ID = 4两大类,主要用于协议内的控制。
- set chunk size (1): 用于设置此 RTMP 会话的块大小。
- abort message (2): 用于取消当前正在发送的消息。
- acknowledgement (3): 用于发送消息确认。
- user control message (4): 用于通知客户端特定事件。
- window acknowledgement size (5): 用于设置接收端的窗口确认大小。
- set peer bandwidth (6): 用于设置对等方的带宽限制。
Set Chunk Size(Message Type ID=1):设置chunk中Data字段所能承载的最⼤字节数,默认为
128B,通信过程中可以通过发送该消息来设置chunk Size的⼤⼩(不得⼩于128B),⽽且通信双⽅会各⾃维护⼀个chunkSize,两端的chunkSize是独⽴的。⽐如当A想向B发送⼀个200B的Message,但默认的chunkSize是128B,因此就要将该消息拆分为Data分别为128B和72B的两个chunk发送FFMPEG推流的时候设置的是 60*1000
其中第⼀位必须为0,chunk Size占31个位,最⼤可代表2147483647=0x7FFFFFFF=2 -1,但实际上所有⼤于16777215=0xFFFFFF的值都⽤不上,因为chunk size不能⼤于Message的⻓度,表示Message的⻓度字段是⽤3个字节表示的,最⼤只能为0xFFFFFF。
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是窗⼝确认消息。
Set Peer Bandwidth(Message Type ID=6):限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的⼤⼩来限制发送端的发送带宽。
limit type:
Hard(Limit Type=0):接受端应该将Window Ack Size设置为消息中的值
Soft(Limit Type=1):接受端可以将Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size⼩与该控制消息中的Window Ack Size)
Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。
数据消息
数据消息
Audio Message(⾳频信息,Message Type ID=8):⾳频数据。Video Message(视频信息,Message Type ID=9):视频数据。
Data Message(数据消息,Message Type ID=15或18):传递⼀些元数据(MetaData,⽐如视频 名,分辨率等等)或者⽤户⾃定义的⼀些消息。当信息使⽤AMF0编码时,Message Type ID=18, AMF3编码时Message Type ID=15.
Aggregate Message (聚集信息,Message Type ID=22):多个RTMP⼦消息的集合
Shared Object Message(共享消息,Message Type ID=16或19):表示⼀个Flash类型的对象,由键值对的集合组成,用于在客户端和服务器之间同步共享对象的状态信息。
当信息使⽤AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16.
- audio message (8): 用于传输音频数据。
- video message (9): 用于传输视频数据。
- data message (18): 用于传输自定义的数据。
- shared object message (19): 用于通知共享对象的变化。
- command message (20): 用于发送命令消息。
AMF0和AMF3简介
AMF (Action Message Format) 是Adobe开发的一种二进制格式,用于序列化ActionScript对象,主要用于Flash和服务器之间的数据交换。在RTMP协议中:
- AMF0:较早的序列化格式,结构简单,支持的数据类型有限,但兼容性好。消息类型值为20(0x14)。
- AMF3:较新的序列化格式,支持更复杂的数据类型和更紧凑的编码,但处理更复杂。消息类型值为17(0x11)。
聚合消息
聚合消息是包含一系列RTMP子消息。消息类型22用于聚合消息。
核心目的:减少网络传输中的块(Chunk)数量,降低协议头部(Header)开销,提升内存和网络效率。
覆盖机制:
- 消息流ID(Stream ID):聚合消息的流ID会覆盖内部所有子消息的流ID,确保子消息属于同一逻辑流。
- 时间戳(Timestamp):聚合消息的时间戳与第一个子消息的时间戳相同,后续子消息的时间戳需通过偏移量重新计算(见下文)。
聚合消息的结构
聚合消息由 多个子消息连续排列 组成,每个子消息包含以下部分:
1. 子消息头部(Sub-Message Header)
-
前一条消息大小(Previous Message Size):4字节,记录前一个子消息的总大小(包含头部),用于兼容FLV格式的向后查找。
-
RTMP头部(Basic Header + Message Header):
-
消息类型(Message Type ID):1字节(如音频
0x08
、视频0x09
、数据0x12
等)。 -
净荷长度(Payload Length):3字节,表示子消息数据部分的长度。
-
时间戳(Timestamp):3字节(或 4字节,若扩展时间戳存在),子消息的原始时间戳。
-
流ID(Stream ID):3字节(通常被聚合消息的流ID覆盖)。
-
2. 子消息数据(Payload)
-
实际音视频数据或控制信息,长度由
Payload Length
定义。
时间戳的规范化处理
时间戳偏移量:聚合消息的时间戳与第一个子消息的时间戳之差(通常为0)。
计算规则:每个子消息的最终时间戳 = 聚合消息时间戳 + 子消息原始时间戳 - 第一个子消息的时间戳。例如:
-
聚合消息时间戳 = 1000 ms。
-
第一个子消息时间戳 = 1000 ms → 偏移量 = 0。
-
第二个子消息时间戳 = 1200 ms → 规范化后时间戳 = 1000 + (1200 - 1000) = 1200 ms。
聚合消息是否属于FLV格式?
聚合消息是RTMP协议层的优化机制,用于提升传输效率。FLV是存储格式,其标签结构独立于RTMP的传输机制。
关联性:聚合消息的设计考虑了与FLV格式的兼容性(如 Previous Message Size
),但它是RTMP协议的一部分,而非FLV格式的直接组成部分。
特性 | RTMP聚合消息 | FLV格式 |
---|---|---|
所属层次 | 传输协议层(动态传输优化) | 存储格式(静态文件或流式存储) |
核心目的 | 减少网络传输开销(合并消息) | 存储音视频数据(文件或录制流) |
结构特点 | 包含多个子消息,带时间戳偏移 | 由标签组成,每个标签独立完整 |
兼容性设计 | 子消息头部兼容FLV的Previous Tag Size | 直接使用RTMP消息转换后的标签 |
应用场景 | 实时推流(如直播) | 视频存储或点播(如录播文件) |
消息优先等级
1.协议先行
协议控制消息(Protocol Control Messages)和用户控制消息(User Control Messages)应该包含消息流ID 0(控制流)和块流ID 2,并且有最高的发送优先级。
2.数据次之
数据消息(音频信息、音频消息)比控制信息的优先级低。
另外,一般情况下,音频消息比视频数据优先级高。通路只有一条(RTMP是单通路)分优先级,优先级高的先行。优先级低的不能阻塞优先级高的。优先级处理是在Message stream层处理的
Chunk报文格式
块格式RTMP Chunk Header的长度不是固定的,分为:
12 Bytes、8 Bytes、4 Bytes、1 Byte 四种(basic默认是1b),由RTMP Chunk
Header前2位决定。
Basic Header
当Basic Header为1个字节时,CSID(Chunk Stream ID)占6位,6位最多可以表示64个数,因此这种情况下CSID在 [0,63] 之间,其中⽤户可⾃定义的范围为 [3,63] ,实际是可以⽤2开始⽤?。
当Basic Header为2个字节时,CSID占只占8位,第⼀个字节除chunk type占⽤的bit都置为0 保留扩展,第⼆个字节⽤来表示CSID-64,8位可以表示 [0, 255] 共256个数,ID的计算⽅法为(第⼆个字节+64),范围为 [64,319]。
当Basic Header为3个字节时,保留扩展1 个字节,以在此字段⽤3字节版本编码。ID的计算⽅法为(第三字节*256+第⼆字节+64)(Basic Header是采⽤⼩端存储的⽅式),范围为 [64,65599]
小结
Basic Header(基本的头信息)首先常用的是1,为什么会分类?主要就是为了减少传输byte
Message Header(消息的头信息)
type=0 占⽤11个字节
Message Header占⽤11个字节,其他三种能表示的数据它都能表示,但在chunk stream的开始的第⼀个chunk和头信息中的时间戳后退(即值与上⼀个chunk相⽐减⼩,通常在回退播放的时候会出现这种情况)的时候必须采⽤这种格式。
timestamp(时间戳 绝对时间 单位ms):占⽤3个字节,因此它最多能表示到16777215=0xFFFFFF=2-1, 当它的值超过这个最⼤值时,这三个字节都置为1,这样实际的timestamp会转存到ExtendedTimestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。
message length(消息数据的⻓度):占⽤3个字节,表示实际发送的消息的数据如⾳频帧、视频帧等数据的⻓度,单位是字节。注意这⾥是Message的⻓度,也就是chunk属于的Message的总数据⻓度,⽽不是chunk本身Data的数据的⻓度。
message type id(消息的类型id):占⽤1个字节,表示实际发送的数据的类型,如8代表⾳频数据、9代表视频数据。
msg stream id(消息的流id):占⽤4个字节,表示该chunk所在的流的ID,和Basic Header的CSID⼀样,它采⽤⼩端存储的⽅式,
Type = 1:占⽤7个字节
省去了表示msg stream id的4个字节,表示此chunk和上⼀次发的chunk所在的流相同,如果在发送端只和对端有⼀个流链接的时候可以尽量去采取这种格式。
Type = 2:占⽤3个字节
type=2时Message Header占⽤3个字节,相对于type=1格式⼜省去了表示消息⻓度的3个字节和表示消 息类型的1个字节,表示此chunk和上⼀次发送的chunk所在的流、消息的⻓度和消息的类型都相同。
Type = 3:占⽤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。
为什么存在不同长度?
一般情况下,msg stream id是不会变的,所以针对视频或音频, 除了第一个RTMP Chunk Header是12Bytes的,后续即可采用8Bytes的。如果消息的长度(message length)和类型(msg type id, 如视频为9或音频为8)又相同,即可将这两部分也省去,RTMP Chunk Header采用4Bytes类型的。如果当前Chunk与之前的Chunk相比, msg stream id相同,msg type id相同,message length相同,而且都属于同一个消息(由同一个Message切割成),这类Chunk的时间戳(timestamp)也是相同的,故后续的也可以省去,RTMP Chunk Header采用1 Byte类型的。
一句话就是利用线性特性,减少传输量,复用前一个包的配置属性。
4种type对⽐
Extended Timestamp(扩展时间戳)
当扩展时间戳启⽤时,timestamp字段或者timestamp delta要全置为0xFFFFFF,表示
应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,⽽不是减去时间戳或者时间戳差的值。
Chunk Data(块数据)
⽤户层⾯上真正想要发送的与协议⽆关的数据,⻓度在(0,chunkSize]之间。
RTMP协议 - 时间戳探讨
时间戳特性
- RTMP中时间戳的单位为毫秒(ms)
- 时间戳为相对于某个时间点的相对值
- 时间戳的长度为32bit,不考虑回滚的话,最大可表示49天17小时2分钟47.296秒
- Timestamp delta单位也是毫秒,为相对于前一个时间戳的一个无符号整数; 可能为24bit或32bit
- RTMP Message的extended时间戳 4个字节
- 大端存储
用wireshark转包分析发现,rtmp流的chunk视频流(或音频流)除第一个视频时间戳为绝对时间戳外,后续的时间戳均为timestamp delta,即当前时间戳与上一个时间戳的差值。
比如帧率为25帧/秒的视频流,timestamp delta基本上都为40ms。通常情况下 -- 3字节timestamp可能为绝对timestamp或timestamp delta。timestamp delta的值超过16777215 (即16进制的0xFFFFFF)时,这时候这三个字节必须被置为: 0xFFFFFF,以此来标示Extended
Timestamp(4字节)将会存在,由Extended Timestamp来表示时间戳
参考资料
RTMP协议学习——Message与Chunk解读 - HolyZion - 博客园
学习资料分享