资讯专栏INFORMATION COLUMN

RTMP H5 直播流技术解析

李世赞 / 2016人阅读

摘要:上一篇文章简单阐述了,在中,做直播需要哪些技术知识点,有哪些直播流协议和技术。搞了一个比较绕的理论,即,通过如下格式中的来确定即,通过位来确定整个的长度。例如最后强调一下,因为规定,的为保留字,所以,不作为。全称为协议控制消息。

上一篇文章简单阐述了,在 H5 中,做直播需要哪些技术知识点,有哪些直播流协议和技术。通过对比,本篇主要聚焦于 RTMP 直播协议的相关内容,也就是说,本篇将会直接进行实际操作 Buffer 的练习和相关的学习。

RTMP 是什么

RTMP 全称即是 Real-Time Messaging Protocol。顾名思义就是用来作为实时通信的一种协议。该协议是 Adobe 搞出来的。主要是用来传递音视频流的。它通过一种自定义的协议,来完成对指定直播流的播放和相关的操作。和现行的直播流相比,RTMP 主要的特点就是高效,这里,我就不多费口舌了。我们先来了解一下 RTMP 是如何进行握手的。

RTMP 握手

RTMP 是基于 TCP 三次握手之后的,所以,RTMP 不是和 TCP 一个 level 的。它本身是基于 TCP 的可靠性连接。RTMP 握手的方式如图:

(C 代表 Client,S 代表 Server)

它主要是通过两端的字段内容协商,来完成可信度认证的。基本过程如下:

client: 客户端需要发 3 个包。C0,C1,C2

server: 服务端也需要发同样 3 个包。 S0,S1,S2。

整个过程如上图所述,但实际上有些细节需要注意。

握手开始:

【1】 客户端发送 C0,C1 包

此时,客户端处于等待状态。客户端有两个限制:

客户端在未接受到 S1 之前不能发送 C2 包

客户端在未接收到 S2 之前不能发送任何实际数据包

【2】 服务端在接受到 C0,发送 S0,S1 包。也可以等到接受到 C1 之后再一起发送,C1 包的等待不是必须的。

此时,服务端处于等待状态。服务端有两个限制:

服务端在未接受到 C1 之前不能发送 S2.

服务端在未接收到 C2 之前不能发送任何实际数据包

【3】客户端接受到 S1/S0 包后,发送 C2 包。

【4】服务端接受到 C2 包后,返回 S2 包,并且此时握手已经完成。

不过,在实际应用中,并不是严格按照上面的来。因为 RTMP 并不是强安全性的协议,所以,S2/C2 包只需要 C1/S1 中的内容,就可以完成内容的拼接。

这么多限制,说白了,其实就是一种通用模式:

C0+C1

S0+S1+S2

C2

接下来,我们来具体看看 C/S 012 包分别代表什么。

C0 && S0

C0 和 S0 其实区别不大,我这里主要讲解一下 C0,就差不多了。首先,C0 的长度为 1B。它的主要工作是确定 RTMP 的版本号。

C0:客户端发送其所支持的 RTMP 版本号:3~31。一般都是写 3。

S1:服务端返回其所支持的版本号。如果没有客户端的版本号,默认返回 3。

C1 && S1

C1/S1 长度为 1536B。主要目的是确保握手的唯一性。格式为:

time: 发送时间戳,这个其实不是很重要,不过需要记住,不要超出 4B 的范围即可。

zero: 保留值 0.

random: 该字段长尾 1528B。主要内容就是随机值,不管你用什么产生都可以。它主要是为了保证此次握手的唯一性,和确定握手的对象。

C2 && S2

C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值。上图也简单说明了就是,对应 C1/S1 的 Copy 值,不过第二个字段有区别。基本格式为:

time: 时间戳,同上,也不是很重要

time2: C1/S1 发送的时间戳。

random: S1/C1 发送的随机数。长度为 1528B。

这里需要提及的是,RTMP 默认都是使用 Big-Endian 进行写入和读取,除非强调对某个字段使用 Little-Endian 字节序。

上面握手协议的顺序也是根据其中相关的字段来进行制定的。这样,看起来很容易啊哈,但是,我们并不仅仅停留在了解,而是要真正的了解,接下来,我们来实现一下,如果通过 Buffer 来进行 3 次握手。这里,我们作为 Client 端来进行请求的发起,假设 Server 端是按照标准进行发送即可。

Buffer 实操握手

我们使用 Buffer 实操主要涉及两块,一个块是 request server 的搭建,还有一块是 Buffer 的拼接。

Request Server 搭建

这里的 Server 是直接使用底层的 TCP 连接。

如下,一个简易的模板:

const client = new net.Socket();

client.connect({
    port: 1935,
    host: "6721.myqcloud.com"},
    ()=>{
        console.log("connected");
    });
    
client.on("data",(data)=>{
    client.write("hello");
});

不过,为了更好的进行实际演练,我们通过 EventEmitter 的方式,来做一个筛选器。这里,我们使用 mitt 模块来做代理。

const Emitter = require("mitt")();

然后,我们只要分析的就是将要接受到的 S0/1/2 包。根据上面的字节包图,可以清楚的知道包里面的详细内容。这里,为了简单起见,我们排除其他协议的包头,只是针对 RTMP 里面的包。而且,我们针对的只有 3 种包,S0/1/2。为了达到这种目的,我们需要在 data 时间中,加上相应的钩子才行。

这里,我们借用 Now 直播的 RTMP 流来进行相关的 RTMP 直播讲解。

Buffer 操作

Server 的搭建其实上网搜一搜,应该都可以搜索出来。关键点在于,如何针对 RTMP 的实操握手进行 encode/decode。所以,这里,我们针对上述操作,来主要讲解一下。

我们主要的工作量在于如何构造出 C0/1/2。根据上面格式的描述,大家应该可以清楚的知道 C0/1/2 里面的格式分别有啥。

比如,C1 中的 time 和 random,其实并不是必须字段,所以,为了简单起见,我们可以默认设为 0。具体代码如下:

class C {
    constructor() {
        this.time;
        this.random;
    }
    C0() {
        let buf = Buffer.alloc(1);
        buf[0] = 3;
        return buf;
    }
    C1() {
        let buf = Buffer.alloc(1536);
        return buf;
    }
    /**
     * write C2 package
     * @param {Number} time the 4B Number of time
     * @param {Buffer} random 1528 byte
     */
    produceC2(){
        let buf = Buffer.alloc(1536);
        // leave empty value as origin time
        buf.writeUInt32BE(this.time, 4);
        this.random.copy(buf,8,0,1528);

        return buf;
    }
    get getC01(){
        return Buffer.concat([this.C0(),this.C1()]);
    }
    get C2(){
        return this.produceC2();
    }
}

接下来,我们来看一下,结合 server 完成的 RTMP 客户端服务。

const Client = new net.Socket();
const RTMP_C = new C();


Client.connect({
    port: 1935,
    host: "6721.liveplay.myqcloud.com"
}, () => {
    console.log("connected")
    Client.write(RTMP_C.getC01);

});

Client.on("data",res=>{
    if(!res){
        console.warn("received empty Buffer " + res);
        return;
    }
    // start to decode res package
    if(!RTMP_C.S0 && res.length>0){
        RTMP_C.S0 = res.readUInt8(0);
        res = res.slice(1);
    }

    if(!RTMP_C.S1 && res.length>=1536){
        RTMP_C.time = res.readUInt32BE(0);
        RTMP_C.random = res.slice(8,1536);
        RTMP_C.S1 = true;
        res = res.slice(1536);
        console.log("send C2");
        Client.write(RTMP_C.C2);
    }

    if(!RTMP_C.S2 && res.length >= 1536){
        RTMP_C.S2 = true;
        res = res.slice(1536);
    }
})

详细代码可以参考 gist。

RTMP 基本架构

RTMP 整个内容,除了握手,其实剩下的就是一些列围绕 type id 的 message。为了让大家更清楚的看到整个架构,这里简单陈列了一份框架:

在 Message 下的 3 个一级子 Item 就是我们现在将要大致讲解的内容。

可以看到上面所有的 item 都有一个共同的父 Item--Message。它的基本结构为:

Header: header 部分用来标识不同的 typeID,告诉客户端相应的 Message 类型。另外,还有个功效就是多路分发。

Body: Body 内容就是相应发送的数据。这个根据不同的 typeID 来说,格式就完全不一样了。

下面,我们先了解一下 Header 和不同 typeID 的内容:

Header

RTMP 中的 Header 分为 Basic Header 和 Message Header。需要注意,他们两者并不是独立的,而是相互联系。Message Header 的结构由 Basic Header 的内容来决定。

接下来,先分开来讲解:

Basic Header

BH(基础头部)主要是定义了该 chunk stream ID 和 chunk type。需要注意的是,BH 是变长度的,即,它的长度范围是 1-3B。怎么讲呢?就是根据不同的 chunk stream ID 来决定具体的长度。CS ID(Chunk Stream ID)本身的支持的范围为 <= 65597 ,差不多为 22bit。当然,为了节省这 3B 的内容。 Adobe 搞了一个比较绕的理论,即,通过如下格式中的 CS ID 来确定:

  0 1 2 3 4 5 6 7
 +-+-+-+-+-+-+-+-+
 |fmt|   cs id   |
 +-+-+-+-+-+-+-+-+

即,通过 2-7 bit 位来确定整个 BH 的长度。怎么确定呢?

RTMP 规定,CS ID 的 0,1,2 为保留字,你在设置 CS ID 的时候只能从 3 开始。

CS ID: 0 ==> 整个 BH 长为 2B,其中可以表示的 Stream ID 数量为 64-319。例如:

  0 1
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt|    0    |    cs id - 64   |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

注意上面的 cs id - 64。这个代表的就是,你通过切割第二个 byte 时,是将得到的值加上 64。即:2th byte + 64 = CS ID

CS ID: 1 ==> 整个 BH 长为 3B。可以存储的 Stream ID 数量为 64-65599。例如:

  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      |           cs id - 64          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

当然,后面 CS ID 的计算方法也是最后的结果加上 64。

CS ID >2 ==> 整个 BH 长为 1B。可以存储的 Stream ID 数量为 3-63。例如:

  0 1 2 3 4 5 6 7
 +-+-+-+-+-+-+-+-+
 |fmt|  cs id    |
 +-+-+-+-+-+-+-+-+

最后强调一下,因为 RTMP 规定,CS ID 的 0,1,2 为保留字,所以,0,1,2 不作为 CS ID。综上所述,CS ID 的起始位为 3(并不代表它是 3 个 Stream)。

上面我并没有提到 fmt 字段,这其实是用来定义 Message Header 的。

Message Header

根据前面 BH 中 fmt 字段的定义,可以分为 4 种 MH(Message Header)。或者说,就是一种 MH 格式会存在从繁到简 4 种:

fmt: 0

当 fmt 为 0 时,MH 的长度为 11B。该类型的 MH 必须要流的开头部分,这包括当进行快退或者点播时重新获取的流。该结构的整体格式如下:

也就是说,当 fmt 为 0 时,其格式是一个完整的 MH。

timestamp 是绝对时间戳。用来代表当前流编码。

message length: 3B, 发送 message 的长度。

type id: 1B

stream id: 4B, 发送 message stream id 的值。是 little-endian 写入格式!

fmt: 1

当 fmt 为 1 时,MH 的长度为 7B。该类型的 MH 不带 msg stream id。msg stream id 由前面一个 package 决定。该数值主要由前一个 fmt 为 0 的 MH 决定。该类型的 MH 通常放在 fmt 为 0 之后。

fmt: 2

当 fmt 为 2 时,MH 的长度为 3B。该类型的 MH 只包括一个 timestamp delta 字段。其它的信息都是依照前面一个其他类型 MH 决定的。

fmt: 3

当 fmt 为 3时,这其实 RTMP 里面就没有了 MH。官方定义,该类型主要全部都是 payload 的 chunk,其 Header 信息和第一个非 type:3 的头一致。因为这主要用于 chunk 中,这些 chunk 都是从一个包里面切割出来的,所以除了第一个 chunk 外,其它的 chunk 都可以采用这种格式。当 fmt 为 3时,计算它的 timestamp 需要注意几点,如果前面一个 chunk 里面存在 timestrameDelta,那么计算 fmt 为 3 的 chunk 时,就直接相加,如果没有,则是使用前一个 chunk 的 timestamp 来进行相加,用代码表示为:

prevChunk.timeStamp += prevChunk.timeStampDelta || prevChunk.timeStamp;

不过,当 fmt: 3 的情况一般很难遇到。因为,他要求前面几个包必须存在 fmt 为 0/1/2 的情况。

接下来的就是 Message Body 部分。

Message Body

上面说的主要是 Message Header 的公用部分,但是,对于具体的 RTMP Message 来说,里面的 type 会针对不同的业务场景有不同的格式。Message 全部内容如上图所示:

这里,我们根据流程图的一级子 item 来展开讲解。

PCM

PCM 全称为:Protocol Control Messages(协议控制消息)。主要使用来沟通 RTMP 初始状态的相关连接信息,比如,windows size,chunk size 等。

PCM 中一共有 5 种不同的 Message 类型,是根据 Header 中的 type ID 决定的,范围是 1~6 (不包括 4)。另外,PCM 在构造的时候需要注意,它 Heaer 中的 message stream id 和 chunk stream id 需要设置为固定值:

message stream ID 为 0

chunk stream ID 为 2

如图所示:

OK,我们接下来一个一个来介绍一下:

Set Chunk Size(1)

看名字大家应该都能猜到这类信息是用来干啥的。该类型的 PCM 就是用来设置 server 和 client 之间正式传输信息的 chunk 的大小,type ID 为 1。那这有啥用呢?

SCS(Set Chunk Size) 是针对正式发送数据而进行数据大小的发送限制。一般默认为 128B。不过,如果 server 觉得太小了,想发送更大的包给你,比如 132B,那么 server 就需要给你发送一个 SCS,告知你,接下来“我发送给你的数据大小是 132B”。

0: 只能设为 0 ,用来表示当前的 PCM 的类型。

chunk size: 用来表示后面发送正式数据时的大小。范围为 1-16777215。

如下,提供过 wireshark 抓包的结果:

Abort Message(2)

该类 PCM 是用来告诉 client,丢弃指定的 stream 中,已经加载到一半或者还未加载完成的 Chunk Message。它需要指定一个 chunk stream ID。

基本格式为:

chunk stream id: 指定丢弃 chunk message 的 stream

Acknowledgement(3)

该协议信息其实就是一个 ACK 包,在实际使用是并没有用到,它主要是用来作为一个 ACK 包,来表示两次 ACK 间,接收端所能接收的最大字节数。

它基本格式为:

sequence number[4B]: 大小为 4B

不过,该包在实际应用中,没有多高的出现频率。

Window Acknowledgement Size(5)

这是用来协商发送包的大小的。这个和上面的 chunk size 不同,这里主要针对的是客户端可接受的最大数据包的值,而 chunk size 是指每次发送的包的大小。也可以叫做 window size。一般电脑设置的大小都是 500000B。

详细格式为:

通过,wireshark 抓包的结果为:

Set Peer Bandwidth(6)

这是 PCM 中,最后一个包。他做的工作主要是根据网速来改变发送包的大小。它的格式和 WAS 类似,不过后面带上了一个 Type 用来标明当前带宽限制算法。当一方接收到该信息后,如果设置的 window size 和前面的 WAS 不一致,需要返回一个 WAS 来进行显示改变。

基本格式为:

其中 Limit Type 有 3 个取值:

0: Hard,表示当前带宽需要和当前设置的 window size 匹配

1: Soft,将当前宽带设置为该信息定义的 window size,或者已经生效的 window size。主要取决于谁的 window size 更小

2: Dynamic,如果前一个 Limit Type 为 Hard 那么,继续使用 Hard 为基准,否则忽略该次协议信息。

实际抓包情况可以参考:

UCM

全称为:User Control Message(用户控制信息)。它的 Type ID 只能为 4。它主要是发送一些对视频的控制信息。其发送的条件也有一定的限制:

msg stream ID 为 0

chunk stream ID 为 2

它的 Body 部分的基本格式为:

UCM 根据 Event Type 的不同,对流进行不同的设置。它的 Event Type 一共有 6 种格式 Stream Begin(0)Stream EOF(1)StreamDry(2)SetBuffer Length(3)StreamIs Recorded(4)PingRequest(6)PingResponse(7)

这里,根据重要性划分,只介绍 Begin,EOF,SetBuffer Length 这 3 种。

Stream Begin: Event Type 为 0。它常常出现在,当客户端和服务端成功 connect 后发送。Event Data 为 4B,内容是已经可以正式用来传输数据的 Stream ID(实际没啥用)。

Stream EOF: Event Type 为 1。它常常出现在,当音视频流已经全部传输完时。 Event Data 为 4B,用来表示已经发送完音视频流的 Stream ID(实际没啥用)。

Set Buffer Length: Event Type 为 3。它主要是为了通知服务端,每毫秒用来接收流中 Buffer 的大小。Event Data 的前 4B 表示 stream ID,后面 4B 表示每毫秒 Buffer 的大小。通常为 3000ms

OK 剩下就是 Command Msg 里面的内容了。

Command Msg

Command Msg 里面的内容,其 type id 涵盖了 8~22 之间的值。具体内容,可以参考下表:

需要注意,为什么有些选项里面有两个 id,这主要和 AMF 版本选择有关。第一个 ID 表示 AMF0 的编解码方式,第二个 ID 表示 AMF3 的编解码方式。
其中比较重要的是 command Msg,video,audio 这 3 个 Msg。为了让大家更好的理解 RTMP 流的解析,这里,先讲解一下 video 和 audio 两个 Msg。

Video Msg

因为 RTMP 是 Adobe 开发的。理所当然,内部的使用格式肯定是 FLV 格式。不过,这和没说一样。因为,FLV 格式内部有很多的 tag 和相关的描述信息。那么,RTMP 是怎么解决的呢?是直接传一整个 FLV 文件,还自定义协议来分段传输 FLV Tag 呢?

这个其实很好回答,因为 RTMP 协议是一个长连接,如果是传整个 FLV 文件,根本没必要用到这个,而且,RTMP 最常用在直播当中。直播中的视频都是分段播放的。综上所述,RTMP 是根据自己的自定义协议来分段传输 FLV Tag 的。那具体的协议是啥呢?

这个在 RTMP 官方文档中其实也没有给出。它只是告诉我们 Video Msg 的 type ID 是 9 而已。

因为,RTMP 只是一个传输工具,里面传什么还是由具体的流生成框架来决定的。所以,这里,我选择了一个非常具有代表性的 RTMP 直播流来进行讲解。

通过 wireshark 抓包,可以捕获到以下的 RTMP 包数据:

这里需要提及一点,因为 RTMP 是主动将 Video 和 Audio 分开传输,所以,它需要交叉发布 Video 和 Audio,以保证音视频的同步。那么具体每个 Video Data 里面的数据都是一样的吗?

如果看 Tag 的话,他们传输的都是 VideoData Tag。先看一下 FLV VideoData Tag 的内容:

这是 FLV Video 的协议格式。但,遇到第一个字段 FrameType 的时候,我们就可能懵逼了,这 TM 有 5 种情况,难道 RTMP 会给你 5 种不同的包吗?

答案是,有可能,但是,很大情况下,我们只需要支持 1/2 即可。因为,视频中最重要的是 I 帧,它对应的 FrameType 就是 1。而 B/P 则是剩下的 2。我们只要针对 1/2 进行软解,即可实现视频所有信息的获取。

所以,在 RTMP 中,也主要(或者大部分)都是传输上面两种 FrameType。我们通过实际抓包来讲解一下。

这是 KeyFrame 的包,注意 Buffer 开头的 17 数字。大家可以找到上面的 FrameType 对应找一找,看结果是不是一致的:

这是 Inter-frame 的包。同上,大家也可以对比一下:

Audio Tag

Aduio Tag 也是和 Video Tag 一样的蜜汁数据。通过观察 FLV Audio Tag 的内容:

上面这些字段全是相关的配置值,换句话说,你必须实现知道这些值才行。这里,RTMP 发送 Audio Tag 和 Video Tag 有点不同。因为 Audio Tag 已经不可能再细分为 Config Tag,所以,RTMP 会直接传递 上面的 audio Tag 内容。详细可以参考抓包内容:

这也是所有的 Audio Msg 的内容。

因为 Audio 和 Video 是分开发送的。所以,在后期进行拼接的时候,需要注意两者的同步。说道这里,顺便补充一下,音视频同步的相关知识点。

音视频同步

音视频同步简单来说有三种:

以 Audio 为准,Video 同步 Audio

以 Video 为准,Audio 同步 Video

以外部时间戳为准,AV 同时同步

主要过程变量参考就是 timeStampduration。因为,这里主要是做直播的,推荐大家采用第二种方法,以 Video 为准。因为,在实际开发中,会遇到 MP4 文件生成时,必须要求第一帧为 keyframe,这就造成了,以 Audio 为参考的,会遇到两个变量的问题。一个是 timeStamp 一个是 keyframe。当然,解决办法也是有的,就是检查最后一个拼接的 Buffer 是不是 Keyframe,然后判断是否移到下一次同步处理。

这里,我简单的说一下,以 Video 为准的同步方法。以 Video 同步,不需要管第一帧是不是 keyframe,也不需要关心 Audio 里面的数据,因为,Audio 数据是非常简单的 AAC 数据。下面我们通过伪代码来说明一下:

// known condition
video.timeStamp && video.perDuration && video.wholeDuration
audio.timeStamp && audio.perDuration

// start
refDuration = video.timeStamp + video.wholeDuration
delta = refDuration - audio.timeStamp
audioCount = Math.round(delta/audio.perDuration);
audDemuxArr = this._tmpArr.splice(0,audioCount);

// begin to demux
this._remuxVideo(vidDemuxArr);
this._remuxAudio(audDemuxArr);

上面算法可以避免判断 Aduio 和 Video timeStamp 的比较,保证 Video 一直在 Audio 前面并相差不远。下面,我们回到 RTMP 内容。来看看 Command Msg 里面的内容。

Command Msg

Command Msg 是 RTMP 里面的一个主要信息传递工具。常常用在 RTMP 前期和后期处理。Command Msg 是通过 AMF 的格式进行传输的(其实就是类似 JSON 的二进制编码规则)。Command Msg 主要分为 net connectnet stream 两大块。它的交流方式是双向的,即,你发送一次 net connect 或者 stream 之后,另外一端都必须返回一个 _result 或者 _error 以表示收到信息。详细结构可以参考下图:

后续,我们分为两块进行讲解:

netConnection

netStream

里面的 _result 和 _error 会穿插在每个包中进行讲解。

NetConnection

netConnection 可以分为 4 种 Msg,connectcallcreateStreamclose

connect

connect 是客户端向 Server 端发送播放请求的。里面的字段内容有:

Command Name[String]: 默认为 connect。表示信息名称

Transaction ID[Number]: 默认为 1。

Command Object: 键值对的形式存放相关信息。

Optional: 可选值。一般没有

那,Command Object 里面又可以存放些什么内容呢?

app[String]: 服务端连接应用的名字。这个主要根据你的 RTMP 服务器设定来设置。比如:live

flashver[String]: Flash Player 的版本号。一般根据自己设备上的型号来确定即可。也可以设置为默认值:LNX 9,0,124,2

tcUrl[String]: 服务端的 URL 地址。简单来说,就是 protocol://host/path。比如:rtmp://6521.liveplay.myqcloud.com/live

fpad[Boolean]: 表示是否使用代理。一般为 false。

audioCodecs[Number]: 客户端支持的音频解码。后续会介绍。默认可以设置为 4071

videoCodecs[Number]: 客户端支持的视频解码。有自定义的标准。默认可以设置为 252

videoFunction[Number]: 表明在服务端上调用那种特别的视频函数。默认可以设置为 1

简单来说,Command Object 就是起到 RTMP Route 的作用。用来请求特定的资源路径。实际数据,可以参考抓包结果:

上面具体的取值主要是根据 rtmp 官方文档来决定。如果懒得查,可以直接使用上面的取值。上面的内容是兼容性比较高的值。当该包成功发送时,另外一端需要得到一个返回包来响应,具体格式为:

Command Name[String]: 为 _result 或者 _error。

Transaction ID[Number]: 默认为 1。

Command Object: 键值对的形式存放相关信息。

Information[Object]: 键值对的形式,来描述相关的 response 信息。里面存在的字段有:level,code,description

可以参考:

connect 包发送的位置,主要是在 RTMP 握手结束之后。如下:

call

call 包主要作用是用来远程执行接收端的程序(RPC, remote procedure calls)。不过,在我解 RTMP 的过程中,并没有实际用到过。这里简单介绍一下格式。它的内容和 connect 类似:

Procedure Name[String]: 调用处理程序的名字。

Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。

Command Object: 键值对的形式存放相关信息。AMF0/3

Optional: 可选值。一般没有

Command Object 里面的内容主要是针对程序,设置相关的调用参数。因为内容不固定,这里就不介绍了。

call 一般是需要有 response 来表明,远端程序是否执行,以及是否执行成功等。返回的格式为:

Command Name[String]: 根据 call 中 Command Object 参数来决定的。

Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。

Command Object: 键值对的形式存放相关信息。AMF0/3

Response[Object]: 响应的结果值

createStream

createStream 包只是用来告诉服务端,我们现在要创建一个 channel 开始进行流的交流了。格式和内容都不复杂:

Procedure Name[String]: 调用处理程序的名字。

Transaction ID[Number]: 自己制定一个。一般可以设为 2

Command Object: 键值对的形式存放相关信息。AMF0/3

当成功后,服务端会返回一个 _result 或者 _error 包来说明接收成功,详细内容为:

Command Name[String]: 根据 call 中 Command Object 参数来决定的。

Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。

Command Object: 键值对的形式存放相关信息。AMF0/3。一般为 Null

Stream ID: 返回的 stream ID 值。

它的返回值很随意,参考抓包内容:

下面,我们来看一下 RTMP 中第二个比较重要的 command msg -- netStream msg。

NetStream Msg

NetStream 里面的 Msg 有很多,但在直播流中,比较重要的只有 play 包。所以,这里我们着重介绍一下 play 包。

play

play 包主要是用来告诉 Server 正式播放音视频流。而且,由于 RTMP 天然是做多流分发的。如果遇到网络出现相应的波动,客户端可以根据网络条件多次调用 play 命令,来切换不同模式的流。

其基本格式为:

Command Name[String]: 根据 call 中 Command Object 参数来决定的。

Transaction ID[Number]: 默认为 0。也可以设置为其他值

Command Object: 不需要该字段,在该命令中,默认设为 Null

Stream Name[String]: 用来指定播放的视频流文件。因为,RTMP 天生是支持 FLV 的,所以针对 FLV 文件来说,并不需要加额外的标识,只需要写明文件名即可。比如:

StreamName: "6721_75994f92ce868a0cd3cc84600a97f75c"

不过,如果想要支持其它的文件,那么则需要额外的表示。当然,音频和视频需要不同的支持:

如果是播放音频文件,比如 mp3,那么则需要额外的前缀标识符-mp3。例如:mp3:6721_75994f9

如果涉及到视频文件的话,不仅需要前缀,还需要后缀。比如播放的是 MP4 文件,则标识为:mp4:6721_75994f9.mp4

startNumber: 这个字段其实有点意思。它可以分为 3 类来讲解:-2,-1,>=0。

-2: 如果是该标识符,服务端会首先寻找是否有对应的 liveStream。没有的话,就找 record_stream。如果还没有的,这次请求会暂时挂起,直到获取到下一次 live_stream。

-1: 只有 live_stream 才会播放。

=0: 相当于就是 seek video。它会直接找到 record_stream,并且根据该字段的值来确定播放开始时间。如果没有的话,则播放 list 中的下一个 video。

durationNumber: 用来设置播放时长的。它里面也有几个参数需要讲解一下,-1,0,>0。

-1: 会一直播放到 live_stream 或者 record_stream 结束。

0: 会播放一段一段的 frame。一般不用。

0: 会直接播放指定 duration 之内的流。如果超出,则会播放指定时间段内容的 record_stream。

reset[Boolean]: 该字段没啥用,一般可以忽略。用来表示否是抛弃掉前面的 playlist。

整个 play 包内容就已经介绍完了。我们可以看看实际的 play 抓包结果:

那 play 包是在那个环节发送,发送完之后需不需要对应的 _result 包呢?

play 包比较特殊,它是不需要 _result 回包的。因为,一旦 play 包成功接收后。server 端会直接开始进行 streamBegin 的操作。

整个流程为:

到这里,后续就可以开始正式接收 video 和 audio 的 stream。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84682.html

相关文章

发表评论

0条评论

李世赞

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<