RTMP 协议精讲

前言

2018 年 12 月,接到了一个调查原有直播系统播放端与编码端时差对不上的任务,于是首先开始学习 RTMP 协议,之后阅读 nginx_rtmp_module 代码,调试程序,调查原因,经过一个多月的努力,幸不辱命,终于查清并解决了该问题,成功为公司的实时直播系统优化掉 10 多毫秒的延时;于是公司安排我讲解 RTMP 协议 和 nginx_rtmp_module 代码结构与流程,遂生成了此篇文章。虽然很多内容来自于网络,但我详细整理和补充了其中的内容,修正了很多网上的错误,望能帮助到各位做流媒体开发的朋友,也请大家能尊重我的辛苦付出,转载请注明出处,谢谢!

作者:杨玉奇
2019 年 1 月 22 日
转发请保留

目录

RTMP 协议

  • 简介
  • 握手包
  • 块格式
  • 协议控制消息
  • AMF 消息

交互过程分析

  • 握手及通用命令
  • Publish 过程分析
  • Play 过程分析

RTMP 协议 – 简介

RTMP 是 Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。是Adobe Systems 公司为 Flash 播放器和服务器之间音频、视频和数据传输 开发的开放协议。该协议基于 TCP,是一个协议族,包括 RTMP 基本协议及 RTMPT/RTMPS/RTMPE等多种变种。

  1. RTMP 工作在 TCP 之上,默认使用端口 1935;
  2. RTMPE 在 RTMP 的基础上增加了加密功能;
  3. RTMPT 封装在 HTTP 请求之上,可穿透防火墙;
  4. RTMPS 类似 RTMPT,增加了 TLS/SSL 的安全功能;

RTMP 协议中数据都是大端


image.png

RTMP 协议 – 握手包

  • C0 / S0
  • C1 / S1
  • C2 / S2
  • 简单握手 / 复杂握手

C0 / S0

C0 和 S0 包由一个字节组成(目前的值应该为 3),下面是 C0/S0 包内的字段:


image.png

在 C0 包内,这个字段代表客户端请求的 RTMP 版本号。在 S0 包内,这个字段代表服务端选择的 RTMP 版本号。此文档使用的版本是 3。版本 0-2 用在早期的产品中,现在已经被弃用;版本 4-31 被预留用于后续产品;版本 32-255(为了区分 RTMP 协议和文本协议,文本协议通常以可打印字符开始)不允许使用。如果服务器无法识别客户端的版本号,应该回复版本 3。客户端可以选择降低到版本 3,或者中止握手过程。

C1 / S1

C1 和 S1 包长度为 1536 字节,包含以下字段:
包内的字段:


image.png

time: 毫秒值的时间戳,这个值可以是 0,或者一些任意值。用于本终端发送的所有后续块的时间起点。
zero: 必须全 0
Random data: 本字段可以包含任意数据。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没必要为此使用加密数据或动态数据。
服务端必须接收到 C0 消息,才能发送 S0 和 S1 消息。

C2 / S2

客户端必须接收到 S1 消息,然后发送 C2 消息。客户端必须接收到 S2 消息,然后发送其他数据。
服务端必须接收到 C1 消息,然后发送 S2 消息。服务端必须接收到 C2 消息,然后发送其他数据。


image.png

time1: 这个字段必须是对端发送过来的时间戳(C1 或 S1 内的)。
time2: 这个字段值为本机接收到对端发送过来的握手包的时刻。
random echo: 这个字段必须是对端发送过来的随机数据。握手的双方可以使用 time1 和 time2 字段来估算网络连接的带宽和/或延迟,但是不一定有用。

简单握手 / 复杂握手

前面描述的握手协议是标准的 Adobe RTMP 文档中描述的内容,我们称为简单握手。
在 Flash 10.1 之后,Adobe 公司改了握手,简单握手不能用了,但是 Adobe 公司并没有修正文档。握手步骤没有变,但内容完全不一样,数据是加密的。
它的步骤是由三个固定大小的块组成,而不是可变大小的块加上头。握手开始于客户端发送 C0 + C1 块。在发送 C2 之前客户端必须等待接收 S1。在发送任何数据之前客户端必须等待接收 S2。服务端在发送 S0 和 S1 之前必须等待接收 C0,也可以等待接收C1。服务端在发送 S2 之前必须等待接收 C1。服务端在发送任何数据之前必须等待接收 C2。
C1 / S1 和 C2 / S2 的 1536 字节是:

  • 4 字节的当前时间:(u_int32_t)time(NULL)
  • 4 字节的程序版本:C1 一般是 0x80000702,S1 是 0x04050001
  • 764 字节的 KeyBlock 或者 DigestBlock
  • 764 字节的 KeyBlock 或者 DigestBlock
    在不同的包里,后两个顺序可能会颠倒
    子结构 KeyBlock 定义:
  • 760 bytes: 包含 128 bytes 的 key 的数据。
  • key_offset: 4 bytes,最后 4 字节定义了 key的 offset(相对于 KeyBlock 开头而言)
    子结构 DigestBlock 定义:
  • digest_offset: 4bytes,开头 4 字节定义了 digest 的 offset(相对于 DigestBlock 的第 5 字节而言,offset=3 表示 digestBlock[7~38] 为 digest
  • 760bytes: 包含 32 bytes 的 digest 的数据。

RTMP 协议 – 块格式

块格式

握手之后,连接复用一个或多个块流。创建的每个块都有一个唯一 ID 对其进行关联,这个 ID 叫做 chunk stream ID (块流 ID),每一类 csid 都对应一种功能。传递时,每个块必须被完全发送才可以发送下一块。在接收端,这些块被根据块流 ID 被组装成消息。
组块允许更高层协议中的大消息分解成较小的消息,例如防止大的、低优先级的消息(如视频)阻塞较小但高优先级的消息,如:音频(高优先级)或控制(中优先级)。块大小是可配置的。


image.png

Basic Header (基本头,1 到 3 个字节):这个字段对块流 ID 和块类型进行编码。长度完全取决于块流 ID,因为块流 ID 是一个可变长度的字段。
Message Header (消息头,0,3,7,或者 11 个字节):这一字段对正在发送的消息 (不管是整个消息,还是只是一小部分) 的信息进行编码。这一字段的长度可以使用块头中定义的块类型进行决定。
Extended Timestamp (扩展 timestamp,0 或 4 字节):这一字段是否出现取决于块消息头中的 timestamp 或者 timestamp delta 字段。它表示的是时间戳的增量,需要与 timestamp 或者 timestamp delta 相加获得总时间戳。
Chunk Data (有效大小):当前块的有效负载,相当于定义的最大块大小。

Basic Header

RTMP 协议最多支持 65597 个流(chunk),CS ID 范围为: 3 ~ 65599。ID 0、1、2 被保留。
0 值表示二字节形式,并且 ID 范围 64 ~ 319 (第二个字节 + 64)。
1 值表示三字节形式,并且 ID 范围为 64 ~ 65599 ((第三个字节) * 256 + 第二个字节 + 64)。3 ~ 63 范围内的值表示整个流 ID。
带有 2 值的块流 ID 被保留,用于下层协议控制消息和命令。
块基本头中的 0 ~ 5 位 (最低有效) 代表块流 ID,块流 ID 2 ~ 63 可以编进这一字段的一字节版本中。


image.png

Basic Header 为 1 字节的情况

image.png

Basic Header 为 2 字节的情况 (第一个字节后 6 位为 000000)

image.png

Basic Header 为 3 字节的情况(第一个字节后 6 位为 000001)

image.png

CSID 保留值

0、1、2 被保留, 3 ~ 8 基本都是固定用途,所以 9 ~ 65599 才用于自定义 csid,但一般我们用不到。

  • 0 表示 Basic Header 总共要占用 2 个字节
  • 1 表示 Basic Header 总共要占用 3 个字节
  • 2 代表该 chunk 是控制信息和一些命令信息
  • 3 代表该 chunk 是客户端发出的 AMF0 命令以及服务端对该命令的应答
  • 4 代表该 chunk 是客户端发出的音频数据,用于 publish
  • 5 代表该 chunk 是服务端发出的 AMF0 命令和数据
  • 6 代表该 chunk 是服务端发出的音频数据,用于 play;或客户端发出的视频数据,用于 publish
  • 7 代表该 chunk 是服务端发出的视频数据,用于 play
  • 8 代表该 chunk 是客户端发出的 AMF0 命令,专用来发送: getStreamLength, play, publish

Message Header

Basic Header fmt 对 Message Header 长度的影响

  • fmt == 0:Message Header 长度为 11
  • fmt == 1:Message Header 长度为 7
  • fmt == 2:Message Header 长度为 3
  • fmt == 3:Message Header 长度为 0
    Chunk Msg Header 可变的原因是为了压缩传输的字节数,把一些相同类型的 chunk 的 head 去掉一些字节,换句话说就是四种类型的包头都可以通过一定的规则还原成 11 个字节,这个压缩和还原在 RTMP 协议中称之为复用/解复用。

fmt == 0

表示 message stream 是个新的流,即使 message stream id 复用了以前的 id。这会是 11 个字节的完整包头。


image.png

timestamp:对于 fmt == 0 的 chunk,绝对时间戳在这里表示,如果时间戳值大于等于 0xffffff(16777215),该值必须是 0xffffff, 且时间戳扩展字段 (Extended Timestamp) 必须发送,其他情况没有要求。
message length:Message 的长度,注意这里的长度并不是跟随 chunk head 其后的 chunk data(Payload)的长度,而是前文提到的一条信令或者一帧视频数据或音频数据的长度。前文提到过信令或者媒体数据都称之为 Message,一条 Message 可以分为一条或者多条 chunk。
message type id:后面有详述。

fmt == 1

长度是 7 bytes 的 chunk head,该类型不包含 stream ID,该 chunk 的 streamID 和前一个 chunk 的 stream ID 是相同的。变长的消息,例如视频流格式,在第一个新的 chunk 以后使用这种类型。注意其中时间戳部分是相对时间。


image.png

timestamp delta : 相对时间,是与上一个 相同 CSID 的 chunk 之间的差值。如果 timestamp delta 大于或等于 16777215(十六进制 0xFFFFFF),这个字段必须是 16777215,指示扩展时间戳字段 (Extended Timestamp) 的存在。

fmt == 2

3 bytes 的 chunk head,从之前具有相同块流 ID 的块中取相应的值。该类型既不包含 stream ID 也不包含消息长度,这种类型用于 stream ID 和前一个 chunk 相同,且有固定长度的信息,例如音频流格式,在第一个新的 chunk 以后使用该类型。


image.png

fmt == 3

0 bytes 的 message head,从之前具有相同块流 ID 的块中取相应的值。当一个单个消息拆成多个 chunk 时,这些 chunk 除了第一个以外,其他的都应该使用这种类型。

Message Type ID

  • 1 设置块大小
  • 2 中断消息,丢弃旧数据
  • 3 确认
  • 4 用户控制消息
  • 5 设置确认窗口大小
  • 6 设置流带宽
  • 7 音频数据
  • 9 视频数据
  • 15(0x0f). AMF3 数据
  • 16(0x10) AMF3 共享对象事件
  • 17(0x11) AMF3 命令
  • 18(0x12) AMF0 数据
  • 19(0x13) AMF0 共享对象事件
  • 20(0x14) AMF0 命令,Invoke 方法调用
  • 22(0x16) 聚合消息, H.264, 类似 FLV 文件存储格式,每个音视频包作为一个 Tag, 许多的 Tag 组成了这个 AMFType=0x16 的数据类型

RTMP 协议 – 协议控制消息

格式

RTMP 块流使用 Message Type ID 1 ~ 6 (见上一节)作为控制消息,这些消息包含了必要的 RTMP 块流协议信息。
这些协议控制消息必须使用使用 2 作为块流 ID(CSID),使用 0 作为消息流 ID(message stream id) 。
协议控制消息接收立即生效;解析时,时间戳字段被忽略。

协议控制消息的 RTMP 头样式:


image.png

设置块大小 Message Type ID=1

chunk 的初始长度固定为 128 个字节,但是这个值是可变的,在客户端和服务端建立连接后,客户端和服务端都可以通过发送信令的方式来通知对端修改 chunk 的长度,理论上来说可以修改 chunk 的最长长度为 65536。这里 chunk 的长度是指 chunk 的数据部分的长度,即 chunk data(payload)的长度。

设置块大小的 RTMP 消息样式:


image.png

chunk size 的特殊规定:


image.png

其中第一位必须为 0,chunk Size 占 31 位,最大可代表 2147483647=0x7FFFFFFF=231-1,但实际上所有大于 16777215=0xFFFFFF 的值都用不上,因为 chunk size 不能大于 Message 的长度,表示 Message 的长度字段是用 3 个字节表示的,最大只能为 0xFFFFFF。

中断消息 Message Type ID=2

当一个 Message 被切分为多个 chunk,接受端只接收到了部分 chunk 时,发送该控制消息表示发送端不再传输同 Message 的 chunk,接受端接收到这个消息后要丢弃这些不完整的 chunk。Data 数据中只需要一个 CSID,表示丢弃该 CSID 的所有已接收到的 chunk。

中断消息的 RTMP 消息样式:


image.png

确认 Message Type ID=3

当收到对端的消息大小等于窗口大小(Window Size)时接受端要回馈一个 ACK 给发送端告知对方可以继续发送数据。窗口大小就是指收到接受端返回的 ACK 前最多可以发送的字节数量,返回的 ACK 中会带有从发送上一个 ACK 后接收到的字节数。

确认的 RTMP 消息样式:


image.png

窗口确认大小 Message Type ID=5

发送端在接收到接受端返回的两个确认间最多可以发送的字节数。

窗口确认大小的 RTMP 消息样式:


image.png

设置带宽 Message Type ID=6

限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的 Window ACK Size 来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的 Window ACK Size 与上一次发送给发送端的 size 不同的话要回馈一个 Window Acknowledgement Size 的控制消息数。

设置带宽的 RTMP 消息样式:


image.png

Limit Type 为 0、1、2 中的一个:

  • 0:Hard,接受端应该将 Window Ack Size 设置为消息中的值
  • 1:Soft,接受端可以将 Window Ack Size 设为消息中的值,也可以保存原来的值(前提是原来的Size 小于该控制消息中的 Window Ack Size)
  • 2:Dynamic,如果上次的设置带宽消息中的 Lim Type 为 0,本次也按 Hard 处理,否则忽略本消息,不去设置 Window Ack Size

RTMP 协议 – AMF 消息

AMF 命令

发送端发送时会带有:

  • 命令的名字,如 connect
  • Transaction ID 表示此次命令的标识
  • Command Object 表示相关参数

接受端收到命令后,会返回以下三种消息中的一种:

  • _result 消息表示接受该命令,对端可以继续往下执行流程
  • _error 消息代表拒绝该命令要执行的操作
  • method name 消息代表要在之前命令的发送端执行的函数名称。
    这三种回应的消息都要带有收到的命令消息中的 Transaction ID 来表示本次的回应作用于哪个命令。�
    可以认为发送命令消息的对象有两种:
  • 一种是 Net Connection,表示双端的上层连接
  • 一种是 Net Stream,表示流信息的传输通道,控制流信息的状态,如 Play 播放,Pause 暂停等。

这些命令直接以 AMF 格式数据放置在 Chunk Data 部分。

AMF 连接层命令

Connect

image.png

Invoke / Call

image.png

_result

如果 Invoke 消息中的 TransactionID 不为 0 的话,对端需要对该命令做出响应,响应的消息结构如下:


image.png

Create Stream

image.png

AMF 流控制命令

建立在 Net Connection 之上,通过 Net Connection 的 Create Stream 命令创建,用于传输具体的音频、视频等信息。在传输层协议之上只能连接一个 Net Connection,但一个 Net Connection 可以建立多个 Net Stream 来建立不同的流通道传输数据。

play

由客户端向服务器发起请求从服务器端接受数据(如果传输的信息是视频的话就是请求开始播流),可以多次调用,这样本地就会形成一组数据流的接收者。注意其中有一个 reset 字段,表示是覆盖之前的播流(设为 true)还是重新开始一路播放(设为 false)。


image.png

play2

可以将当前正在播放的流切换到同样数据但不同比特率的流上,服务器端会维护多种比特率的文件来供客户端使用 play2 命令来切换。


image.png

delete Stream

用于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此路流。


image.png

receiveAudio

通知服务器端该客户端是否要发送音频。


image.png

receiveVideo

通知服务器端该客户端是否要发送视频。


image.png

publish

由客户端向服务器发起请求推流到服务器。


image.png

seek

定位到视频或音频的某个位置,以毫秒为单位。


image.png

Pause

客户端告知服务端停止或恢复播放。


image.png

onStatus

客户端向服务端发送流控制命令后,服务端会通过 onStatus 的命令来响应客户端,表示当前 Net Stream 的状态。


image.png

onStatus 状态值:

  • "NetStream.Play.Reset" --- 播放列表重置
  • "NetStream.Play.Start" --- 播放开始
  • "NetStream.Buffer.Empty" --- 视频正在缓冲
  • "NetStream.Buffer.Full" --- 缓冲区已填满
  • "NetStream.Play.StreamNotFound" --- 找不到此视频
  • "NetStream.Play.Stop" --- 视频播放完成
  • "NetStream.Pause.Notify" --- 流已暂停
  • "NetStream.Unpause.Notify" --- 流已恢复
  • "NetStream.Seek.Failed" --- 搜寻失败
  • "NetStream.SeekStart.Notify" --- 搜寻开始
  • "NetStream.Seek.Notify" --- 正在搜寻
  • "NetStream.Seek.Complete" --- 搜寻完毕
  • "NetStream.Publish.Start" --- 发布开始
  • "NetStream.Unpublish.Success" --- 停止发布
  • "NetStream.Record.Start" --- 开始录制
  • "NetStream.Record.Stop" --- 停止录制
  • "NetStream.Publish.BadName" --- 警告!试图发布已经被他人发布的流
  • "NetStream.Play.PublishNotify" --- 发布开始,信息已经发送到所有订阅者
  • "NetStream.Play.UnpublishNotify" --- 发布停止,信息已经发送到所有订阅者
  • "NetStream.Play.InsufficientBW" --- 警告!客户端没有足够的带宽,无法以正常速度播放数据"

AMF 共享对象消息

共享对象是 Flash 对象,可以通过多客户端,实例同步传输。在 AMF0 编码中,类型为 19;在 AMF3 编码中,类型为 16。每个消息可以包含多个事件。

消息格式:


image.png

共享对象消息支持的事件类型:

  • Use (=1):The client sends this event to inform the server about the creation of a named shared object.
  • Release (=2):The client sends this event to the server when the shared object is deleted on the client side.
  • Request Change (=3): The client sends this event to request that the change the value associated with a named parameter of the shared object.
  • Change (=4): The server sends this event to notify all clients, except the client originating the request, of a change in the value of a named parameter.
  • Succes s(=5): The server sends this event to the requesting client in response to RequestChange event if the request is accepted.
  • SendMessage (=6): The client sends this event to the server to broadcast a message. On receiving this event, the server broadcasts a message to all the clients, including the sender.
  • Status (=7): The server sends this event to notify clients about error conditions.
  • Clear (=8): The server sends this event to the client to clear a shared object. The server also sends this event in response to Use event that the client sends on connect.
  • Remove (=9): The server sends this event to have the client delete a slot
  • Request Remove (=10): The client sends this event to have the client delete a slot.
  • Use Success (=11): The server sends this event to the client on a successful connection.

AMF 音频消息

publish:
CSID = 04, Message Type ID = 08
play:
CSID = 06, Message Type ID = 08

RTMP HEADER 举例:

  • fmt == 0: 04 000000 000007 08 01000000
  • fmt == 0: 04 000000 00001c 08 01000000
  • fmt == 1: 44 000017 00000b 08
  • fmt == 3: c4
  • fmt == 2: 84 000018
  • fmt == 2: 84 000017
  • fmt == 3: c4

AMF 视频消息

publish:
CSID = 06, Message Type ID = 09
play:
CSID = 07, Message Type ID = 09

RTMP HEADER 举例:

  • fmt == 0: 07 000000 00002c 09 01000000
  • fmt == 1: 47 000000 00cbf1 09
  • fmt == 3: c7
  • fmt == 3: c7
  • fmt == 1: 47 000028 0014b7 09
  • fmt == 3: c7

AMF 集合消息

集合消息是一个独立消息,包含了一系列的 RTMP 消息。此消息 Message Type ID = 22。集合消息由消息头和消息内容组成。消息内容由子消息组成,子消息由消息头,消息数据,回放指针组成。


image.png

集合消息的消息流 ID 覆盖此消息内的子消息流的 ID。集合消息和第一个子消息的时间戳之间的偏移量,用来将子消息的时间戳处理为流的时间刻度。每个子消息的时间戳可以通过添加偏移量来处理为正常的流时间。第一个子消息的时间戳应该和集合消息的时间戳相同,因此偏移量应该为零。
反向指针包含了以前的消息(包含头信息)的大小。集合消息包含此字段,一是为了适配 FLV 文件格式,二是为了回放定位。
使用集合消息有如下几个优势:
块流在一个块内至多可以携带一条完整的消息。使用集合消息之后,不仅可以增加块大小,同时还减少了发送的块数量。
集合消息的子消息可以连续的存储在内存中。当系统调用网络发送数据时更高效。

AMF 用户控制消息

CSID = 2,Message Type ID = 4


image.png

CMD ID:

  • Stream Begin = 0 ,流开始,4 字节数据: stream ID
  • Stream EOF = 1 ,流结束,4 字节数据: stream ID
  • StreamDry = 2 ,流中没有更多数据,4 字节数据: stream ID
  • SetBuffer = 3,设置缓冲区长度(以毫秒为单位)8 字节数据:stream ID + 长度
  • StreamIs Recorded = 4,4 字节数据:stream ID
  • PingRequest = 6,4 字节数据:时间戳
  • PingResponse = 7,4 字节数据:时间戳

AMF 数据

AMF0

  • AMF_NUMBER = 0, 数字(double),8 字节
  • AMF_BOOLEAN = 1, 布尔,1 字节
  • AMF_STRING = 2, 字符串,后面跟 2 字节长度 + 数据
  • AMF_OBJECT = 3, 对象,包含多组属性名(都是字符串)与值的对应数据:2 字节长度 + 属性名 + AMF 数据,0x000009 表示嵌套结束
  • AMF_MOVIECLIP = 4, 保留, 未使用
  • AMF_NULL = 5, NULL,0 字节
  • AMF_UNDEFINED = 6, 未定义
  • AMF_REFERENCE = 7, 引用
  • AMF_ECMA_ARRAY = 8, 数组
  • AMF_OBJECT_END = 9, 对象结束
  • AMF_STRICT_ARRAY = 0x0a, 严格的数组
  • AMF_DATE = 0x0b, 日期
  • AMF_LONG_STRING = 0x0c, 长字符串
  • AMF_UNSUPPORTED = 0x0d, 未支持
  • AMF_RECORDSET = 0x0e, 保留, 未使用
  • AMF_XML_DOC = 0x0f, xml 文档
  • AMF_TYPED_OBJECT = 0x10, 有类型的对象
  • AMF_AVMPLUS = 0x11, 需要扩展到 AMF3
  • AMF_INVALID = 0xff, 无效的

实例


image.png

交互过程分析

握手及通用命令

  • 客户端发:C0 + C1
  • 服务端发:S0 + S1 + S2
  • 客户端发:C2
  • 客户端发:connect
  • 服务端发:设置应答窗口大小
  • 服务端发:设置流带宽
  • 服务端发:设置 chunk 块大小
  • 服务端发:_result('NetConnection.Connect.Success')

之后 publisher 和 player 的命令就不相同了

Publish 过程分析

  • 客户端发:设置 chunk 块大小
  • 客户端发:releaseStream
  • 客户端发:FCPublish
  • 客户端发:createStream
  • 服务端发:_result()
  • 客户端发:publish
  • 服务端发: onStatus('NetStream.Publish.Start')
  • 客户端发:@setDataFrame
  • 客户端发:音/视频数据

之后就是不断的发送音视频数据了

Play 过程分析

  • 客户端发:设置应答窗口大小
  • 客户端发:createStream
  • 服务端发:_result()
  • 客户端发:getStreamLength()
  • 客户端发:play
  • 客户端发:Set Buffer
  • 服务端发:onStatus('NetStream.Play.Start')
  • 服务端发:|RtmpSampleAccess()
  • 服务端发:Stream Begin
  • 服务端发:onMetaData()
  • 服务端发:音/视频数据

之后就是不断的发送音视频数据了

你可能感兴趣的:(RTMP 协议精讲)