【音视频协议篇】RTMP协议

目录

🌈前言🌈

[📁 RTMP概念](#📁 RTMP概念)

[📁 RTMP握手](#📁 RTMP握手)

[📁 RTMP建立连接](#📁 RTMP建立连接)

[📁 RTMP建流 & play](#📁 RTMP建流 & play)

[📁 Message 和 Chunk](#📁 Message 和 Chunk)

[📂 Message](#📂 Message)

[📂 Chunk](#📂 Chunk)

[📁 总结](#📁 总结)


🌈前言🌈

本文主要讲解音视频协议,RTMP协议。该协议作为直播广泛使用的协议,重要性不言而喻。

这篇文章是我在学习该协议时,根据网络上的资料以及RTMP协议规范所整理的笔记,我认为还是比较基础的,从开零开始学习RTMP协议。

📁 RTMP概念

Real Time Messaging Protocol,实时消息传输协议。默认端口号为1935。是一个应用层协议,基于TCP协议。协议的基本数据单元是消息(Message),传输的过程中消息会被拆分为更小的消息块(Chunk)单元。最后将分割后的消息块通过TCP协议传输,接收端再将接收的消息块恢复成流媒体数据。

核心应用场景

  • 直播推流:游戏直播、秀场直播中主播端推流至服务器,依赖其低延迟保障操作与声音的实时同步。

  • 实时互动场景:视频会议、在线教育中教师与学生的音视频实时交互,确保低延迟对话。

  • 安防监控:摄像头实时传输画面至监控中心,稳定性要求高。

  • 局限:依赖Flash(逐渐淘汰),防火墙穿透能力弱(需RTMPT/RTMPS变体)

    因此RTMP协议主要作为推流协议来使用。推流:将音视频数据传输到服务器。

推流过程:

RTMP协议规定,播放一个流媒体要有两个前提步骤:

  1. 建立一个网络连接(NetConenction)

  2. 建立一个网络流(NetStream)

RTMP协议需要客户端和服务器通过 "握手" 来建立基于TCP连接之上的 RTMP Connection连接 。在Connection连接上传输一些控制消息。其中CreateStream消息会创建一个Stream流 ,用于传输具体的 数据消息(音视频数据)命令消息(控制这些消息传输)
总结一下,RTMP协议推流的步骤:

1. 握手(TCP三次握手 + RTMP握手)

2. 建立连接(建立客户端和服务器之间的网络连接)

3. 建立流(建立客户端和服务器之间的网络流)

4. 推流&播放

📁 RTMP握手

客户端要向服务器发送C0,C1,C2 三个Chunk;服务器向客户端发送S0,S1,S2三个Chunk,然后才能进行有效的信息传输。

RTMP协议本身没有规定这6个Message的具体传说顺序,但是RTMP协议实现者必须保证:

  1. 客户端要等收到S1后才能发送C2

  2. 客户端要等收到S2后才能发送其他信息(控制信息和真实的音视频数据)

  3. 服务端要等收到C0或C1后才能发送S0,S1

  4. 服务端要等收到C1后才能发送S2

  5. 服务端必须等收到C2后才能发送其他信息(控制信息或真实的音视频数据)

  • Uninitialized (未初始化): 客户端发送C0包(1 字节,版本信息),如果服务器支持这个版本会响应S0和S1,否则终止连接。

  • Version Sent (版本已发送): 当服务器接收到版本号后(已发送S0和S1),客户端等S1,服务器等C1,当都接收后,客户端发送C2,服务器发送S2,然后两者状态变成Ack Sent。

  • Ack Sent (确认已发送): 客户端和服务器分别等待 S2 和 C2。

    cpp 复制代码
        +-+-+-+-+-+-+-+-+-+-+
        |   time (4 bytes)  |
        +-+-+-+-+-+-+-+-+-+-+
        |   zero (4 bytes)  |
        +-+-+-+-+-+-+-+-+-+-+
        |   random bytes    |
        +-+-+-+-+-+-+-+-+-+-+
        |random bytes(cont) |
        |       ....        |
        +-+-+-+-+-+-+-+-+-+-+
      
    time: 时间戳,表示发送这个数据块的端点的后续块的时间起始点,可以是0,或其他任何数。
    zero: 本字段必须全部是0。
    random bytes:1528B, 该字段可以包含任何值。
  • Handshake Done (握手结束): 客户端和服务器可以开始交换消息了。

C0和S0格式(1B):

cpp 复制代码
+-+-+-+-+-+-+-+-+
| version |
+-+-+-+-+-+-+-+-+

C0:表示客户端请求的RTMP版本号
S0: 服务端选择的RTMP版本号

C1和S1格式:

cpp 复制代码
    +-+-+-+-+-+-+-+-+-+-+
    |   time (4 bytes)  |
    +-+-+-+-+-+-+-+-+-+-+
    |   zero (4 bytes)  |
    +-+-+-+-+-+-+-+-+-+-+
    |   random bytes    |
    +-+-+-+-+-+-+-+-+-+-+
    |random bytes(cont) |
    |       ....        |
    +-+-+-+-+-+-+-+-+-+-+
  
time: 时间戳,表示发送这个数据块的端点的后续块的时间起始点,可以是0,或其他任何数。
zero: 本字段必须全部是0。
random bytes:1528B, 该字段可以包含任何值。

C2和S2格式:

cpp 复制代码
 +-+-+-+-+-+-+-+-+-+-+
 |   time (4 bytes)  |
 +-+-+-+-+-+-+-+-+-+-+
 |   time2(4 bytes)  |
 +-+-+-+-+-+-+-+-+-+-+
 |   random bytes    |
 +-+-+-+-+-+-+-+-+-+-+
 |random bytes(cont) |
 |       ....        |
 +-+-+-+-+-+-+-+-+-+-+
 
time: 必须包含对端发送的时间(对于C2来说是S1的time;对于S2来说是C1的time)
time2: 必须包含之前发送的并被对端读取的包的时间戳
random bytes:包含对端发送的随机数据字段(C2:S1 ; S2:C1)

📁 RTMP建立连接

  1. 客户端发送命令消息中的 "连接" (connect) 到服务器,请求与一个服务应用实例建立连接.

  2. 服务器收到连接命令消息后,**"发送确认窗口大小" (Window Acknowledgement Size)**协议消息到客户端,同时连接到连接命令汇总提到的应用程序.

  3. 服务器发送 设置带宽协议 (Set Peer Bandwidth ) 消息到客户端.

  4. 客户端处理 设置带宽协议消息 后,发送 **确认窗口发小(Window Acknowledgement Size)**协议消息到服务端.

  5. 服务器发送用户控制消息的 **"流开始"(Stream Begin)**消息到客户端.

  6. 服务器发送命令中的 "结果"(_result) ,通知客户端连接的状态.

📁 RTMP建流 & play

  1. 客户端发送 "建立流"(createStream) 命令;服务端返回 建立流命令成功结果 (_result)。

  2. 客户端发送 play命令。

  3. 服务端收到play命令后,发送一个**设置块大小(SetChunkSize)**消息。

  4. 服务器发送另一个用户控制信息,指定事件**"流记录"(StreamIsRecorded)**和 流ID。这个消息的头2字节携带事件类型,最后4字节携带流ID。

  5. 服务端发送另一个用户控制消息,指定事件 "流开始"(StreamBegin)。向客户端指示流的开始。

  6. 如果客户端发送的 播放(play)命令成功,服务端发送命令消息(onStatus),NeStream.Play.Start & NeStream.Play.Reset。

  7. 如果客户端发送的play命令设置了resest标志时,服务器才会发送NeStream.Play.Reset。

  8. 如果没有找到要播放的流,服务器发送 onStatus消息NeStream.Play.StreamNotFound。

  9. 之后,客户端播放服务器发送的音视频数据

📁 Message 和 Chunk

📂 Message

RTMP协议会对数据进行格式化,这些格式化的消息称为 RTMP Message ,而实际传输中为了更好的实现多路复用,分包和消息的公平性,发送端会把Message划分为带有Message ID的Chunk.

每个Chunk可能都是一个单独的Message,也可能是Message的一部分,在接收端会根据Chunk包含的message id和message length等信息把Chunk还原为完整的Message,实现消息的收发。

cpp 复制代码
 Message = Message Header + Message Payload
 Message Header = Message Type + Payload Length + Timestamp + Stream ID
   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                |           
  +---------------------------------------------------------------+  
  |                            Timestamp                          | 
  +-----------------------------------------------+---------------+ 
  |                    Stream ID                  |               :   
  +-----------------------------------------------+ - - - - - - - +
  :                         Message Payload                       :
  + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
  :                              ...                              |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
(1)Message Type(1 byte)
以下简写为MT消息类型很重要,它代表了这个消息是什么类型,当写程序的时候需要根据不同的消息,做不同的处理

(2)Payload length(3 bytes) 
     表示负载的长度(big-endian 格式) 

(3)Timestamp (4 bytes) 
     时间戳(big-endian 格式)

(4)Stream ID (3 bytes) 
     消息流ID(big-endian 格式)

(5)Message Payload 
     真实的数据

消息主要分为3类:协议控制消息,数据消息,命令消息等。

协议控制消息

协议控制消息是用来与对端协调控制的。MT的范围1~7。1~2 用于chunk协议。3~6 用于rtmp协议本身,协议控制消息必须要求Message Stream ID=0 和 Chunk Stream ID=2

cpp 复制代码
MT=1, Set Chunk Size 设置块的大小,通知对端用使用新的块大小,共4 bytes。默认大小是128字节  
 +-------------+----------------+-------------------+----------------+  
  | Basic header|Chunk Msg Header|Extended Timestamp | Set chunk size |  
  +-------------+----------------+-------------------+----------------+ 
MT=2, Abort Message 取消消息,用于通知正在等待接收块以完成消息的对等端,丢弃一个块流中已经接收的部分并且取消对该消息的处理,共4 bytes。
  +-------------+----------------+-------------------+----------------+ 
  | Basic header|Chunk Msg Header|Extended Timestamp | Chunk Stream ID|  
  +-------------+----------------+-------------------+----------------+ 
MT=3, Acknowledgement 确认消息,客户端或服务端在接收到数量与窗口大小相等的字节后发送确认消息到对方。窗口大小是在没有接收到接收者发送的确认消息之前发送的字节数的最大值。服务端在建立连接之后发送窗口大小。本消息指定序列号。序列号,是到当前时间为止已经接收到的字节数。共4 bytes。
  +-------------+----------------+-------------------+----------------+ 
  | Basic header|Chunk Msg Header|Extended Timestamp | Sequence Number|  
  +-------------+----------------+-------------------+----------------+ 
MT=4, User Control Message 用户控制消息,客户端或服务端发送本消息通知对方用户的控制事件。本消息承载事件类型和事件数据。消息数据的头两个字节用于标识事件类型。事件类型之后是事件数据。事件数据字段是可变长的。
 +-----------+--------------+----------------+-----------+-----------+  
|Basic header|ChunkMsgHeader|ExtendedTimestamp|Event Type|Event Datar|  
 +-----------+--------------+----------------+-----------+-----------+ 
MT=5, Window Acknowledgement Size 确认窗口大小,客户端或服务端发送本消息来通知对方发送确认消息的窗口大小,共4 bytes.
 +-----------+--------------+-----------------+-----------------------+ 
|Basicheade|ChunkMsgHeader|ExtendedTimestamp|WindowAcknowledgementSize| 
 +-----------+--------------+-----------------+-----------------------+ 
MT=6, Set Peer Bandwidth 设置对等端带宽,客户端或服务端发送本消息更新对等端的输出带宽。发送者可以在限制类型字段(1 bytes)把消息标记为硬(0),软(1),或者动态(2)。如果是硬限制对等端必须按提供的带宽发送数据。如果是软限制,对等端可以灵活决定带宽,发送端可以限制带宽?。如果是动态限制,带宽既可以是硬限制也可以是软限制。
+-------------+----------------+-------------------+----------------------------+------------+  
| Basic header|Chunk Msg Header|Extended Timestamp | Window Acknowledgement 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。 

音频数据消息

MT=8, Audio message, 客户端或服务端发送本消息用于发送音频数据。消息类型 8 ,保留为音频消息。

视频数据消息

MT=9, Video message, 客户端或服务端使用本消息向对方发送视频数据。消息类型值 9 ,保留为视频消息。

命令消息

MT=18/19, NetConnection 和 NetStream 的所有控制命令(如 connectplayonStatus)均通过 RTMP 的 Command Message传输。

📂 Chunk

RTMP收发数据的时候,并不是以Message为单位,而是把Mesage拆分成Chunk发送,必须在一个Chunk发送完成后才能开始发送下一个Chunk。每个Chunk带有MessageId代表属于哪个Message,接收端也会根据这个Id将Chunk组成Message。

为什么要拆分Messsage,因为可以避免优先级低的消息持续发送阻塞优先级高的数据。例如,视频传输过程中,会包含视频帧,音频帧和控制信息,如果一直发送音频帧或控制信息,会造成观看视频的卡顿现象。同时对于数据量较小的Message,可以通过Chunk Header字段来压缩信息,减少信息的传输量。

Chunk的默认大小是128字节 ,在传输过程中,通过一个叫做Set Chunk Size 的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。在实际传输中根据当前的带宽信息和实际信息的大小来动态调整Chunk的大小,从而提高CPU的利用率并减少阻塞几率。

cpp 复制代码
+-------+     +--------------+----------------+  
| Chunk |  =  | Chunk Header | Chunk Data     |  
+-------+     +--------------+----------------+
+--------------+     +-------------+----------------+-------------------+ 
| Chunk Header |  =  | Basic header| Message Header |Extended Timestamp |  
+--------------+     +-------------+----------------+-------------------

Basic Header(基本的头信息 1~3byte)

这块字段对块流ID和块类型进行编码,长度完全取决于块流ID,因为块流ID是一个可变长度的字段。

cpp 复制代码
  +-+-+-+-+-+-+-+-+
  |fmt|   cs id   |
  +-+-+-+-+-+-+-+-+
chuck stream = cs
fmt: 表示块类型,决定了Chunk Msg Header的格式,它占第一个字节的0~1bit,
cs id:表示块流id 占2~7bit属于csid。

Chunk Message Header(块消息的消息头信息 0,3,7,11 byte)

包含了要发送的实际信息(可能完整,也可能不完整)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt字段控制。

cpp 复制代码
 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:Message Header 长度为 11。

cpp 复制代码
| Timestamp (3) | Message Length (3) | Message Type ID (1) | Stream ID (4) |

fmt == 1:Message Header 长度为 7。

cpp 复制代码
| Timestamp Delta (3) | Message Length (3) | Message Type ID (1) |

fmt == 2:Message Header 长度为 3。

cpp 复制代码
| Timestamp Delta (3) |

fmt == 3:Message Header 长度为 0,复用前一个 Chunk 的头部信息。

Extended Timestamp(扩展时间 0,4 byte)

cpp 复制代码
 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 的块一定不能含有本字段。
本字段在块消息头之后,块数据之前。

注意:

1. Chunk Stream ID 和 Message Stream ID:

2. 消息的发送优先级:

📁 总结

以上,就是我在学习RTMP协议时,整理的笔记,欢迎大家在评论区指出文章的错误,一起学习。

相关推荐
c7691 小时前
【文献笔记】ARS: Automatic Routing Solver with Large Language Models
人工智能·笔记·语言模型·自然语言处理·llm·论文笔记·cvrp
胖虎12 小时前
(十九)深入了解 AVFoundation-编辑:使用 AVMutableVideoComposition 实现视频加水印与图层合成(上)——理论篇
音视频·视频编辑·视频水印·视频动画
后端小张2 小时前
智谱AI图生视频:从批处理到多线程优化
开发语言·人工智能·ai·langchain·音视频
Tracy9732 小时前
A316-Mini-V1:超小尺寸USB高清音频解码器模组技术探析
嵌入式硬件·音视频·智能硬件·xmos 模组
hjjdebug2 小时前
用ffmpeg 进行视频的拼接
ffmpeg·音视频·文件拼接
cherishSpring2 小时前
Redis学习笔记
redis·笔记·学习
喜欢你,还有大家4 小时前
Linux笔记2——常用命令-1
linux·服务器·笔记
Ro Jace4 小时前
图像分析学习笔记(2):图像处理基础
图像处理·笔记·学习
我爱学嵌入式6 小时前
C语言第 4 天学习笔记:位运算、流程控制与输入输出
linux·c语言·笔记
Star在努力7 小时前
C语言:第11天笔记
c语言·开发语言·笔记