流媒体学习4
5)StreamBegin
StreamBegin属于用户控制类消息,header的typeid为0x04。而用户控制消息的类型的定义如下:
如此,我们就得出了StreamBegin的过滤条件。接下来我们看看StreamBegin消息,还是先看一下抓包文件:
RTMP服务器发送StreamBegin以通知客户端流已经可以使用并且可以用于通信。默认情况下,从客户端成功接收到connect命令后,将在ID 0上发送StreamBegin。StreamBegin的数据字段占用4字节,其类型占用2个字节,所以RTMP Body部分总共占用6个字节(类型+数据)。其中数据字段代表已开始运行的流的流ID,此例中为1。
7.createStream
创建完RTMP连接之后就可以创建或者访问RTMP流,对于推流端,客户端要向服务器发送一个releaseStream命令消息,之后是createStream命令消息,对于拉流端,则要发送play消息请求视频资源。我们先来看看推流端的消息流程,当发送完createStream消息之后,解析服务器返回的消息会得到一个stream ID, 这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
1)createStream
我们来先看看createStream消息,RTMP客户端发送此消息到服务端,创建一个逻辑通道,用于消息通信。音频、视频、元数据均通过createStream创建的数据通道进行交互,而releaseStream与createStream相对应,为什么有的时候会在createStream之前先来一次releaseStream呢?这就像我们很多的服务实现中,先进行一次stop,然后再进行start一样。因为我们每次开启新的流程,并不能确保之前的流程是否正常走完,是否出现了异常情况,异常的情况是否已经处理等等,所以,做一个类似于恢复初始状态的操作,releaseStream就是这个作用。
createStream的整体架构:
使用字符串类型标识命令的类型,恒为“createStream”;然后使用number类型标识事务ID;紧接着是command相关的信息,如果存在,用set表示,如果没有,则使用null类型表示。
该createStream消息中,事务ID为2,没有command相关的信息,使用Null类型表示。命令的名称(0x02)、事务ID(0x00)、命令的信息(0x03 or 0x05)均按照AMF0的格式进行编码,我们从抓包文件中也可以比较明显的看到。
2)_result/_error
客户端发送createStream请求之后,服务端会反馈一个结果给客户端,如果成功,则返回_result,如果失败,则返回_error。返回的消息的整体结构如下图:
整体与createStream类似,commandName为固定为"_result"或者"_error",transcationID为createStream中的ID,此例中为2,comamndObject也是与createStream一样的组织方式。不同的是多了一个streamID,成功的时候,返回一个streamID,失败的时候返回失败的原因,类似于错误码。
3)releaseStream
说完createStream,我们就可以说说releaseStream命令了。关于releaseStream命令,我们还是通过抓包文件来进行分析,首先看一个releaseStream的抓包文件。
release消息的组织结构,comandName + transactionID + commandObject + 流的一些用户名和密码信息等(可能没有),简单示意图如下:
commandName恒为“releaseStream”用以区分类型,transactionID指明要释放的stream,此处id为2,也就是我们前面createStream的id(在客户端请求下一个createStream的时候,就会先释放原有的stream),comandObject携带一些相关的信息(可能没有);userPass部分为针对流的一些用户名和密码的一些信息,本例中没有,所以,字符串中的长度为0。
8.publish推流
1) publish
对于推流端,经过releaseStream,createStream消息之后,得到了_result消息之后,接下来客户端就可以发起publish消息。推流端使用publish消息向rtmp服务器端发布一个命名的流,发布之后,任意客户端都可以以该名称请求视频、音频和数据。我们首先来看一下publish消息的组织结构:
- commandName:使用string类型,表示消息类型(“publish”);
- transactionID:使用number类型表示事物ID;
- commandObject:对于publish消息,该部分为空,用null类型表示;
- publishName:发布的流的名称,使用string类型表示,比如我们发布到rtmp://192.168.1.101:1935/rtmp_live/test,则test为流名称,也可以省略,此时该字段为空字符;
- publishType:发布的流的类型,使用string类型表示,有3种类型,分别为live、record、append。record表示发布的视频流到rtmp服务器application对应的目录下会将发布的流录制成文件,append表示会将发布的视频流追加到原有的文件,如果原来没有文件就创建,live则不会在rtmp服务器上产生文件。
通过抓包文件,我们可以看到这一抓包文件发送一条publish的消息,事务id为5,没有指定发布的流的名称,发布流的方式是live。如果发布的地址为rtmp://192.168.1.101:1935/rtmp_live,则其他任何客户端都可以访问该url获取视频资源,进而进行播放。
2)onStatus
客户端发送publish消息给rtmp服务端后,服务端会向客户端反馈一条消息,该消息采用了onStatus,onStatus的消息格式如下:
onStatus消息由三部分组成:
- command Name:表示消息类型,恒为“onStatus”;
- transaction ID:设为0;
- command Object:用null表示;
- info Object:使用object类型表示多个字段,一般有warn,status,code,description等状态。
该抓包文件使用onStatus返回了一条消息,描述的状态内容中code为NetStream.Publish.Start,description为Start publishing,该消息的目的就是告诉推流客户端,现在可以推流了。
3)SetDataFrame/OnMetaData
一般在客户端收到服务端返回的针对publish的onStatus消息之后,如果没有异常,推流端还会向服务器发送一条SetDataFrame的消息,其中包含onMetaData消息,这一条消息的主要作用是告诉服务端,推流段关于音视频的处理采用的一些参数,比如音频的采样率,通道数,帧率,视频的宽,高等信息。
RTMP Body部分首先以AMF0格式(string)表示SetDataFrame消息;然后以AMF0格式编码(string)表示onMetaData消息;最后描述具体的关于音视频相关的参数,该部分使用ECMA Array的类型来表示。
SetDataFrame部分
onMetaData部分
property部分
ECMA Arrya本身的类型为0x08,其后紧跟着的是数组元素的个数,此处为20,占用4个字节表示,在之后便是具体的数组中的每一个元素。而数组中的每一个元素的具体编码方式又是遵循AMF0编码标准的。此例中,共表示了20个属性。包含文件大小,视频宽度和高度,视频编码codec_id,帧率信息,比特率信息,音频的codec_id,音频采样率,channel数量等,最后还有一个encoder字段来表示编码器,我们推流使用的是obs(open broad cast),所以该字段中表明了使用obs-output module,obs的版本号为25.0.0。
9.play拉流
1) 综述
在客户端发起createStream命令之后,客户端收到服务端反馈的_result消息,接下来客户端就可以向服务端发起请求播放的指令,这个指令就是play。首先我们看一下官方给出的关于play的消息流示意图。
首先我们来简单介绍一下关于play的流程,客户端向服务端发送play指令之后,服务端收到之后向客户端发送SetChunkSize消息,实际场景中大都在服务器回复客户端connect消息的时候一起发送setChunkSize消息;
服务端向客户端发送StreamIsRecorded消息(实际场景中比较少见);服务端向客户端发送StreamBegin消息,向客户端指示流传输的开始;StreamIsRecoreded消息和StreamBegin消息组织结构比较简单,RTMP Body部分使用2个字节表示事件类型,StreamBegin的类型为0x00,4个字节表示StreamID。
说明:streamBegin wireshark过滤条件
rtmpt.ucm.eventtype == 0x00
客户端成功发送play请求后,服务端向客户端发送onStatus命令消息NetStream.Play.Start 和 NetStream.Play.Reset消息。其中NetStream.Play.Reset消息只有在客户端发送play消息的时候设置了reset标志的时候才会发。如果客户端请求播放的流不存在,服务端会返回onStatus命令消息NetStream.Play.StreamNotFound。
这些交互结束之后,服务端就会向客户端发送音频和视频数据,客户端就可以进行解码,然后渲染播放了。接下来我们来看一下这几条消息(streamBegin前面已介绍):
2) play
play消息的整体组织结构
- commandName:命令的名称,为“connect”;
- transaction ID:事务ID,用number类型表示;
- command Object:如果有,用object类型表示,如果没有,则使用null类型指明;
- stream Name:请求的流的名称,一般在url中application后面的字段,如rtmp://192.17.1.202:1935/rtmp_live/test,rtmp_live为application,test为流的名称;
- start:可选字段,使用number类型表示,指示开始时间,默认值为-2,表示客户端首先尝试命名为streamName的实时流(官方文档中说以秒单位,实际抓包文件中看到的单位应该是毫秒,要注意);
- duration:可选字段,用number类型表示,指定播放时间,默认值为-1,表示播放到流结束;
- reset:可选字段,用boolean类型表示,用来指示是否刷新之前的播放列表;
该抓包文件中的play命令,transactionID为4,commandObject中没有内容,streamName为“test”,没有duration和reset字段,有start字段,默认值为-2000,2000毫秒,即2s。
3) setChunkSize ![https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9rYVk0SkcxdE9EVHZwOVdiOGlhRHdhYVljVmoxUVIzM1ZpYVluV3lBZUN3MG03dVlTSzltc1J6YTl4MzhHM3MwMlRBOG9Tc0YwQW9BVVhIRzhZNlZzT0hBLzY0MA?x-oss-process=image/format,png](https://img2023.cnblogs.com/blog/2525040/202312/2525040-20231208142228839-1214440130.png)
setChunkSize消息结构也比较简单,RTMP Header中的typeID,用来表示消息类型,setChunkSize的类型为0x01。RTMP Body部分直接用4个字节表示chunk size,此例中chunkSize设为4000。
说明:setChunkSize消息wireshark中的过滤条件为:
rtmpt.header.typeid == 0x01
4)onStatus-play start
如果没有任何异常情况,服务器会向客户端发送一个onStatus的状态,如果没有异常,该状态的描述为play start。
onStatus消息的组织结构如下:
onStatus消息由三部分组成:
- command Name:表示消息类型,恒为“onStatus”;
- transaction ID:设为0;
- command Object:用null表示;
- info Object:使用object类型表示多个字段,一般有warn,status,code,description等状态。
command Name, command Object,transaction ID我们已经很熟悉了。我们来看看info Object中的内容,按照AMF0的格式编码,开头表示类型0x03,之后有3个字段描述object;最后有一个End of Obejct Maker用来标记。对于play命令的请求,回应的level为status,code为NetStream.Play.Start,desription为Start live。这些表示play请求命令成功,接下来可以进行音视频的播放了。
10 audio
rtmp协议wireshark中过滤音频数据包的条件为:
rtmpt.header.typeid == 0x08
通过抓包文件,我们看到音频数据也是按照RTMP Header + Rtmp Body的组织结构来进行封装的。Header部分之前的文章解析过,我们主要来看Body部分。因为rtmp是Adobe公司开发的协议,所以对自己东西当然是青睐有加,音频的数据的Body部分正是按照FLV的格式进行组装的。而Flv的封装以tag为单位来进行组织,对于音频数据,包含tagHeader + tagData,tagHeader占用一个字节,表明音频编码的相关参数,tagData为具体的音频编码数据。
小的红色框中的数据即为audioTag的header,此处值为0xaf。接下来,我们就看下flv中audioTag的Header是如何组织的。
tag占用1个字节,我们从高到低,依次来看:
1)音频编码格式
高4比特,用于表示音频编码格式,具体可选值如下。
2)音频采样率
在之后的2个bit,表示音频采样率。
3)位深
之后的1个bit表示采样位深度,可选值为0,1,0表示8比特深度,1表示16比特深度。
4)声道
之后的1个bit表示声道数的参数,可选值为0,1,0表示sndMono,1表示sndStereo。
5)举例
rtmp Body中的数据是audio类型,audio类型的第一个字节表示header,其值为0xaf=0x10101111,将二进制隔开为4段:
0x1010=10
0x11=3
0x1=1
0x1=1
我们可以得出,该音频书包的编码格式为AAC,采样率为44KHz,位深度为16bit,声道模式为strereo。
11. videoData
通过抓包文件,我们可以看到,熟悉的Rtmp Header + Rtmp Body的组织结构,Body中打包的是经过压缩的视频数据。Body中打包视频数据的方式也与音频类似。首先用一个字节表示视频数据的header,之后是压缩后的视频数据(压缩后的数据是使用FLV的标准进行封装的)。
我们来看一下videoData中的header部分的组织结构:
相比音频比较简单,1个字节,高4位表示视频帧类型,低4位表示codecID。
1)帧类型
表示该帧视频是关键帧还是非关键帧。可选值如下:
这么多值,当前主要用的无非就是1和2,即H264的关键帧和非关键帧,其他类型当下几乎见不到了。
2)codecID
表示该帧的数据编码codecID。可选值如下:
对于codecID,其实我们主要关注AVC即可,它代表的是H264编码。
3)例子
可以看出videoData中Body中,第一个字节为0x17,二进制为:0001 0111。所以type=0001=1,codec_id=0111=7,所以表示该视频数据采用H264编码,该帧是关键帧。
在头信息之后,就是具体的压缩后的视频编码数据,不过基于FLV的格式对视频数据做了封装,这里就不展开了。