1、推流链路
RTMP 协议是基于可靠传输 TCP 协议的,所以首先离不开创建 TCP 连接。RTMP 流建立过程大概要经历三个阶段:
- 第一阶段是 TCP 连接建立过程。
- 第二阶段是 RTMP HandShake 过程,本人理解主要是为了连接探测,因为 tcp 连接 ok ,不代表可以正常通信。比如,某个实验小程序占用了 1935 端口,connect 能成功,但是 RTMP 通信肯定不行,因为不支持此协议啊,所以需要探测这一步。
c0,c1,c2 是客户端要发送的消息,s0,s1,s2 是服务端要发送的消息。rtmp 1.0 规范中的例子是每一个包都是单发的,顺序是 c0->s0->c1->s1->c2->s2。然而,具体实现都是如下图所以,目的是为了减少交互过程,加快连接过程。
握手大概过程:
Client Server
| |
| C0, C1 |
|---------------------->|
| |
| S0,S1,S2 |
|<----------------------|
| |
| C2 |
|---------------------->|
- 第三阶段是逻辑流通道建立过程,对于 play 和 publish 过程稍有不同。
play 过程:
Client Server
| |
| connect(app,tcUrl, codecs) |
|------------------------------------------>|
| |
| Window Acknowledgement Size |
|<------------------------------------------|
| |
| Set Peer Bandwidth (0x06) |
|<------------------------------------------|
| |
| -result(response connect) |
|<------------------------------------------|
| |
| Window Acknowledgement Size |
|------------------------------------------>|
| |
| createStream |
|------------------------------------------>|
| |
| _result(response createStream) |
|<------------------------------------------|
| |
| getStreamLength() |
|------------------------------------------>|
| |
| play(stream name) |
|------------------------------------------>|
| |
| User Control Message set buffer length |
|------------------------------------------>|
| |
| Stream Begin |
|<------------------------------------------|
| |
| onStatus | RtmpSampleAccess | onMetaData |
|<------------------------------------------|
| |
| Media Data |
|<------------------------------------------|
| |
2、推流交互消息流程
librtmp提供了推流的API,可以在rtmp.h文件中查看所有API。我们只需要使用常用的几个API就可以将streaming推送到服务器。
- RTMP_Init()//初始化结构体
- RTMP_Free()
- RTMP_Alloc()
- RTMP_SetupURL()//设置rtmp server地址
- RTMP_EnableWrite()//打开可写选项,设定为推流状态
- RTMP_Connect()//建立NetConnection
- RTMP_Close()//关闭连接
- RTMP_ConnectStream()//建立NetStream
- RTMP_DeleteStream()//删除NetStream
- RTMP_SendPacket()//发送数据
3、Connect 消息
RTMP 命令消息格式:
+----------------+---------+---------------------------------------+
| Field Name | Type | Description |
+--------------- +---------+---------------------------------------+
| Command Name | String | Name of the command. Set to "connect". |
+----------------+---------+---------------------------------------+
| Transaction ID | Number | Always set to 1. |
+----------------+---------+---------------------------------------+
| Command Object | Object | Command information object which has |
| | | the name-value pairs. |
+----------------+---------+---------------------------------------+
| Optional User | Object | Any optional information |
| Arguments | | |
+----------------+---------+---------------------------------------+
RTMP握手之后先发送一个connect命令消息,命令里面包含什么东西,协议中没有具体规定,实际通信中要携带 rtmp url 中的 appName 字段,并且指定一些编解码的信息,并以AMF格式发送。
这些信息协议中并没有特别详细说明, 在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消息,没有错误就可以进行下一步了。
4、Create Stream 消息
创建完RTMP连接之后就可以创建RTMP流,客户端要向服务器发送一个releaseStream命令消息,之后是FCPublish命令消息,在之后是createStream命令消息。
当发送完createStream消息之后,解析服务器返回的消息会得到一个stream ID。
这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
5、 Publish Stream
推流准备工作的最后一步是Publish Stream,即向服务器发一个publish命令消息,消息中会带有流名称字段,即rtmp url中的 streamName,这个命令的message stream ID 就是上面 create stream 之后服务器返回的stream ID,发完这个命令一般不用等待服务器返回的回应,直接发送音视频类型的RTMP数据包即可。有些rtmp库还会发setMetaData消息,这个消息可以发也可以不发,里面包含了一些音视频meta data的信息,如视频的分辨率等等。
6、推送音视频
当以上工作都完成的时候,就可以发送音视频了。音视频RTMP消息的Payload(消息体)中都放的是按照FLV-TAG格式封的音视频包,具体可以参照FLV封装的协议文档。格式必须封装正确,否则会造成播放端不能正常拿到音视频数据,无法播放音视频。
5. 关于RTMP的时间戳
RTMP的时间戳单位是毫秒ms,在发送音视频之前一直为零,发送音视频消息包后时候必须保证时间戳是单调递增的,时间戳必须打准确,否则播放端可能出现音视频不同步的情况。Srs-librtmp的源码中,如果推的是视频文件的话,发现他们是用H264的dts作为时间戳的。实时音视频传输的时候是先获取了下某一时刻系统时间作为基准,然后每次相机采集到的视频包,与起始的基准时间相减,得到时间戳,这样可以保证时间戳的正确性。
7、关于Chunk Stream ID
RTMP 的Chunk Steam ID是用来区分某一个chunk是属于哪一个message的 ,0和1是保留的。每次在发送一个不同类型的RTMP消息时都要有不用的chunk stream ID, 如上一个Message 是command类型的,之后要发送视频类型的消息,视频消息的chunk stream ID 要保证和上面 command类型的消息不同。每一种消息类型的起始chunk 的类型必须是 Type_0 类型的,表明新的消息的起始。