以下内容部分来源于网络,自己还未读过相关的底层,所以可能会有错误,自己以后会回来再继续更改的。
大家都会关注“在浏览器输入一个地址,然后回车,会发生什么”这样一个问题,但是有没有想过这样一个问题:主播开始直播,用户打开客户端观看,这个过程发生了什么?
随着技术的发展,直播技术对人们生活的渗透日益加深。从最开始的游戏直播,到前几天爆出来的教育直播,甚至现在都有直播招聘。
而我们喜欢的这些直播,他们用到的传输协议有一个通用名-流媒体传输协议。
要认识流媒体协议,就离不开下面的三大系列名词。
博主记得小时候,经常会玩一种叫动感画册的东西。一本很小的画册上,每一页都画了一幅图,用手快速的翻过每一页,就能看到一个很短的“动画片”。
没错,咱们看到的视频,本质上就是一连串快速播放的图片。
每一张图片,我们称为一帧。只要每秒钟的数据足够多,也就是播放速度足够快,人眼就看不出是一张张独立的图片。对于人眼而言,这个播放临界速度是每秒 30 帧,而这里的 30 也就是我们常说的帧率(FPS)。
每一张图片,都是由像素组成,而每个像素又是由 RGB 组成,每个 8 位,共 24 位。
我们假设一个视频中的所有图片的像素都是 1024*768,可以大概估算下视频的大小:
每秒钟大小 = 30 帧 x 1024 x 768 x 24 = 566,231,010 Bits = 70,778,880 Bytes
按我们上面的估算,一分钟的视频大小就是 4,246,732,800 Bytes,这里已经有 4 个 G 了。
是不是和我们日常接触到的视频大小明显不符?这是因为我们在传输的过程中,将视频压缩了。
为什么要压缩视频?按我们上面的估算,一个一小时的视频,就有 240G,这个数据量根本没办法存储和传输。因此,人们利用编码技术,给视频“瘦身”,用尽量少的 Bit 数保持视频,同时要保证播放的时候,画面仍然很清晰。实际上,编码就是压缩的过程。
我们之所以能够对视频流中的图片进行压缩,因为视频和图片有下列这些特点:
从上面这些特点中可以看出,用于编码的算法非常复杂,而且多种多样。虽然算法多种,但编码过程实际上是类似的,如下图:
视频编码的算法这么多,能不能形成一定的标准呢?当然能,这里咱们就来认识下视频编码的两大流派。
视频经过编码之后,生动活泼的一帧帧图像就变成了一串串让人看不懂的二进制。这个二进制可以放在一个文件里,然后按照一定的格式保存起来,这里的保存格式,就是系列名词一。
编码后的二进制文件就可以通过某种网络协议进行封装,放在互联网上传输,这个时候就可以进行网络直播了。
网络协议将编码好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络数据包,从而得到里面的视频流,这个过程称为接流。
服务端接到视频流之后,可以滴视频流进行一定的处理,比如转码,也就是从一个编码格式转成另一种格式,这样才能适应各个观众使用的客户端,保证他们都能看到直播。
流处理完毕后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求视频流的过程称为拉流。
如果有非常多的观众同时看一个视频直播,都从一个服务器上拉流,压力就非常大,因此需要一个视频的分发网络,将视频预先加载到就近的边缘节点,这样大部分观众就能通过边缘节点拉取视频,降低服务器的压力。
当观众将视频流拉下来后,就需要进行解码,也就是通过上述过程的逆过程,将一串串看不懂的二进制转变成一帧帧生动的图片,在客户端播放出来。
整个直播过程,可以用下图来描述:
接下来,我们依次来看一下每个过程:
虽然我们说视频是一张张图片的序列,但如果每张图片都完整,就太大了,因而会将视频序列分成三种帧:
可以看出,I 帧最完整,B 帧压缩率最高,而压缩后帧的序列,应该是 IBBP 间隔出现。这就是通过时序进行编码。
在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大图分解成一个个小块,可以方便进行空间上的编码。如下图:
尽管时空非常立体的组成了一个序列,但总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的网络提取层单元(NALU,Network Abstraction Layer Unit)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。
如上图,每个 NALU 首先是一个起始标识符,用于标识 NALU 之间的间隔。然后是 NALU 的头,里面主要配置了 NALU 的类型。最后的 Payload 里面是 NALU 承载的数据。
在 NALU 头里面,主要的内容是类型 NAL Type,其中:
在传输视频流之前,剥削要传输者两类参数,不然就无法解码。为了保证容错性,每一个 I 帧之前,都会传一遍这两个参数集合。
如果 NALU Header 里面的表示类型是 SPS 或 PPS,则 Payload 中就是真正的参数集的内容。
如果类型是帧,则 Payload 中是真正的视频数据。当然也是一帧帧保存的。前面说了,一帧的内容还是挺多的,因而每一个 NALU 里面保存的是一片。对于每一片,到底是 I 帧,还是 P 帧,亦或是 B 帧,在片结构里面也有 Header,这里面有个类型用来标识帧的类型,然后是片的内容。
这样,整个格式就出来了。一个视频,可以拆分成一系列的帧,每一帧拆分成一系列的片,每一片都放在一个 NALU 里面,NALU 之间都是通过特殊的起始标识符分隔,在每一个 I 帧的第一片前面,要插入单独保存 SPS 和 PPS 的 NALU,最终形成一个长长的 NALU 序列。
形成 NALU 序列后,还需要将这个二进制的流打包成网络包进行发送。这里我们以 RTMP 协议为例,进入第二个过程,推流。
RTMP 是基于 TCP 的,因而也需要双方建立一个 TCP 连接。在有 TCP 的连接的基础上,还需要建立一个 RTMP 连接,也就是在程序里面,我们调用 RTMP 类库的 Connet 函数,显式创建一个连接。
RTMP 为什么需要建立一个单独的连接呢?
因为通信双方需要商量一些事情,保证后续的传输能正常进行。其实主要就是两个事情:
沟通这些事情,需要发送 6 条消息:
客户端发送 C0、C1、C2
服务端发送 S0、S1、S2
首先,客户端发送 C0 表示自己的版本号,不必等对方回复,然后发送 C1 表示自己的时间戳。
服务器只有在收到 C0 的时候,才会返回 S0,表明自己的版本号,如果版本不匹配,可以断开连接。
服务器发送完 S0 后,也不用等待,就直接发送自己的时间戳 S1。
客户端收到 S1 时,发一个知道了最烦时间戳的 ACK C2。同理,服务器收到 C1 的时候,发一个知道了对方时间戳的 ACK S2。
于是,握手完成。
握手之后,双方需要互相传递一些控制信息,例如 Chunk 块的大小、窗口大小等。
真正传输数据的时候,还是需要创建一个流 Stream,然后通过这个 Stream 来推流。
推流的过程,就是讲 NALU 放在 Message 里面发送,这个也称为 RTMP Packet 包。其中,Message 的格式就像下图所示:
发送的时候,去掉 NALU 的起始标识符。因为这部分对于 RTMP 协议来讲没有用。接下来,将 SPS 和 PPS 参数集封装成一个 RTMP 包发送,然后发送一个个片的 NALU。
RTMP 在收发数据的时候并不是以 Message 为单位的,而是把 Message 拆分成 Chunk 发送,而且必须在一个 Chunk 发送完成之后,才能开始发送下一个 Chunk。每个 Chunk 中都带有 Message ID,表示属于哪个 Message,接收端也会按照这个 ID 将 Chunk 组装成 Message。
前面连接的时候,设置 Chunk 块大小就是指这个 Chunk。将大的消息变为小的块再发送,可以在低带宽的情况下,减少网络拥塞。
面用一个分块的示例,来了解下 RTMP 是如何分块的。
假设一个视频的消息长度是 307,而 Chunk 大小约定为 128,那么消息就会被拆分为 3 个 Chunk。
第一个 Chunk 的 Type = 0,表示 Chunk 头是完整的。头里面 Timestamp 为 1000,总长度 Length 为 307,类型为 9,是个视频,Stream ID 为 12346,正文部分承担 128 个字节的 Data。
第二个 Chunk 也要发送 128 个字节,但是由于 Chunk 头和第一个一样,因此它的 Chunk Type = 3,表示头和第一个 Chunk 一样。
第三个 Chunk 要发送的 Data 的长度为 51 个字节,Chunk Type 还是用的 3。
就这样,数据源源不断的到达流媒体服务器,整个过程就像下图:
这个时候,大量观看直播的观众就可以通过 RTMP 协议从流媒体服务器上拉取。为了减轻服务器压力,我们会使用分发网络。
分发网络分为中心和边缘两层。边缘层服务器部署在全国各地及横跨各大运营商里,和用户距离很近。而中心层是流媒体服务集群,负责内容的转发。
智能负载均衡系统,根据用户的地理位置信息,就近选择边缘服务器,为用户提供推/拉流服务。中心层也负责转码服务。例如,将 RTMP 协议的码流转换成 HLS 码流。
接下来,我们再来看看观众通过 RTMP 拉流的过程。
先读到的是 H.264 的解码参数,例如 SPS 和 PPS,然后对收到的 NALU 组成一个个帧,进行解码,交给播放器播放,这样客户端就能看到直播视频了。