# 13|SVC:如何实现视频编码可伸缩? 你好,我是李江。 前面我们用了4节课的时间分别讲述了如何将视频编码码流打包成H264,如何预测网络带宽,如何做好码控来控制视频发送的速率,如何分析视频的花屏和卡顿等问题。基本上循序渐进地将视频传输中最重要的一些知识点都讲解了一遍,并对里面几个重要的算法进行了深入的研究。 今天,我们再讲述一个视频会议场景中经常会使用的视频编码传输相关的技术——SVC编码,也叫做可伸缩视频编码。它的作用是可以实现在一个码流里面包含多个可解码的子码流,服务器可以根据接收端的网络状况,下发对应码率的码流,从而实现可伸缩性。 ## 为什么需要SVC 2020年全球爆发新冠疫情,很多公司为了员工的安全,实行在家办公的政策。视频会议一时成为了工作中必不可少的日常工作活动。很多大型公司可能会出现一次几十、上百个人参加视频会议的情况。对于视频会议技术商来说,如何提供几十、上百个人的高质量视频通话技术是一个难题。为什么呢? 比如说,我和你两个人进行视频通话,我是发送端,网络非常好,你是接收端,网络比较差。发送端和接收端之间的视频通路如下图所示: ![图片](https://static001.geekbang.org/resource/image/bb/d2/bb477d3a740c1b5a4a9c41e33c1043d2.jpeg?wh=1920x620) 在[带宽预测](https://time.geekbang.org/column/article/467073)这节课里面我们讲过,由于服务器到接收端的网络比较差,那么最后会引起: * 一组视频RTP包的接收时长很长,而一组视频RTP包的发送时长比较小; * 或者发送端的视频RTP包发送给接收端之后,网络中丢包率很高。 如果不做带宽预测和码控的话最终接收端看到发送端的画面会非常卡。 当然,我们肯定是会做带宽预测和码控的。遇到这种情况,发送端通过基于延时和基于丢包的带宽预测算法估算出发送端到接收端之间的网络带宽值。得到这个带宽值之后,参考[第11讲](https://time.geekbang.org/column/article/468091),发送端的视频码控算法就会将码率降下来,同时,码率下降引起QP上升,画面质量下降,但是流畅性变好,不会一直卡死。 这是1对1视频会议场景遇到网络不好时的拥塞控制策略。 想象一下,现在有10个人参加视频会议,我是主持人,也是视频发送端。我们简化一下场景,我的画面需要发给其他的9个观众,而观众的画面不会发送给其他人。10个人都在自己的家里面办公,每个人的网络状况都不一样。有的人网络非常好,带宽有100M,而有的人网络非常差,只有500~600k,而且还因为使用无线网络丢包率很高。而我的网络比较中等,2M带宽,有线网络丢包率非常小。 那现在开始视频会议,带宽预测算法开始工作,预测出我和其他9个人之间的链路带宽有2M、1M、500k、800k等很多个不同的带宽大小。假设忽略音频和其它数据占用的码率,所有的带宽都设置给视频的话,那请问现在设置给视频码控算法的目标码率应该是多少? ![图片](https://static001.geekbang.org/resource/image/d5/c5/d51443baf475861fbbc96ddf28ec10c5.jpeg?wh=1920x1080) 设置为2M?那么带宽只有500k、800k的人看到的画面就是一直卡死的。那我们选择最小值500k不就可以了吗?是的,选择最小值500k,那么所有人都可以流畅地看到画面。可是画面非常糊,质量很差。 想象一下,如果100人参加视频会议,99个人的网络带宽是100M,一个人带宽是500k,最后我们选择500k的视频码率,合理吗?其他99个人为这一个人的网络不好一直观看质量非常差的画面。这太不公平了。有什么办法能很好地解决这个问题呢?这就是今天的主角——SVC。 ## 什么是SVC SVC是指一个码流当中,我们可以分成好几层。比如说分成三层: * 第0层是最底层,可以独立进行编解码,不依赖第1层和第2层; * 第1层编解码依赖于第0层,但是不依赖于第2层; * 第2层的编解码需要依赖于第0层和第1层; 并且,**第0层质量最低,第0层加第1层次之,三层加在一起的时候质量最高。**注意这里的质量不是直接指的画面质量,而是帧率、分辨率的高低所代表的质量。 这样分层有什么好处呢?好处就是我们编码一个码流,可以组合出好几个不同的可解码码流出来。比如说上面三层SVC的例子:第0层就是一个可以独立解码的码流;第0层加上第1层也是一个可以独立解码的码流;第0层加上第1层和第2层也是一个可以解码的码流。 对于网络差的人,服务器给他转发第0层码流对应的RTP包;对于网络中等的人来说,服务器给他转发第0层加第1层码流对应的RTP包;对于网络很好的人,服务器给他直接转发所有层码流的RTP包。这样是不是就对大家都比较公平了。那具体怎么实现SVC分层编码呢?服务器又怎么转发呢?这里我给出我的思路,可供你参考。 ## SVC的分类 根据是在帧率上做SVC还是在分辨率上做SVC,我们可以将SVC分为时域SVC和空域SVC两种。接下来我们逐一看一下。 ### 时域SVC 首先,第一种SVC分层编码方式是我们可以在帧率上做SVC,这种SVC称之为时域SVC。 帧率上做SVC是什么意思呢?我们在前面的课中讲过,一般我们在RTC场景中选择使用连续参考的参考结构来做编码。如下图所示: ![图片](https://static001.geekbang.org/resource/image/80/f3/801bc1fa0198e04020b8ef14803cb9f3.jpeg?wh=1920x363) 这种参考结构非常简单,但是有一个很大的问题就是只要有一帧被丢弃或不完整,就会导致后面的帧都不能解码,强行解码就会出现花屏(可以参考[第07讲](https://time.geekbang.org/column/article/463775)和[第12讲](https://time.geekbang.org/column/article/469197))。 因此,如果是这种编码参考结构的话,就没有可伸缩性了。也就会产生前面多人视频会议的问题。我们把参考帧结构稍微换一下,隔一帧参考一帧,变成一个两层的结构,就可以解决连续参考的问题,如下图所示: ![图片](https://static001.geekbang.org/resource/image/8c/5a/8c345746fc50cc31ac9305981e5f9e5a.jpeg?wh=1920x497) 在图中,帧0是I帧不需要参考,且是第0层的帧。帧1是P帧,参考帧0,且是第1层的帧。帧2是P帧,参考帧0,不参考帧1,是第0层的帧。帧3是P帧,参考帧2,是第1层的帧。一直用这种模式不断地循环下去。 下面我们再来看一个三层时域SVC编码的参考帧结构,如下图所示: ![图片](https://static001.geekbang.org/resource/image/e0/d3/e0dd56dda75da348a203df34633a69d3.jpeg?wh=1920x606) 在图中,帧0是I帧不需要参考,是第0层的帧。帧1是P帧,参考帧0,与两层时域SVC不同,它是第2层的帧。帧2是P帧,参考帧0,不参考帧1,是第1层的帧。帧3是P帧,参考帧1,是第2层的帧。帧4是P帧,参考帧0,是第0层的帧,帧5是P帧,参考帧4,是第2层的帧。不断按照这个模式循环下去。 这个就是时域SVC编码。它的优点是什么呢?**它通过调整参考帧结构就能实现分层编码。低层的帧不会参考高层的帧。**如果我们丢弃高层的帧,低层的帧也是可以顺利地完成解码而不会出现花屏的,只是帧率会降低。但是相比连续参考结构中丢失一帧就直接卡死的体验要好很多。 同时,因为只需要调整一下参考结构,本身常用的编码标准都支持这种参考帧选择的方式,是符合常规标准的。因此,解码器都支持,没有兼容性问题。 但是它也有缺点。我们在[第07讲](https://time.geekbang.org/column/article/463775)中提到过,一般自然运动是连续的,选择前一帧作为参考帧一般压缩率会比较高,因为前后相邻的两帧很相似。**而时域SVC这种跨帧参考的方式会使得压缩率有一定的下降。**两层SVC编码效率大概下降10%,三层大概下降15%。 ### 空域SVC 下面,我们介绍另一种SVC编码,空域SVC。**空域SVC是在分辨率上做分层。**比如说,我们现在需要编码一个720P的视频。我们分成两层:第0层是360P的分辨率;第0层加第1层是720P的分辨率。如下图所示: ![图片](https://static001.geekbang.org/resource/image/d3/79/d35b23ccaa472b80f32cc20944229679.jpeg?wh=1920x669) **空域SVC的优点也是我们可以在一个码流当中分出多个码流出来。**比如说,两层空域SVC,第0层是一个可以独立解码的码流,只是分辨率是360P。第1层依赖于第0层,两个层次加起来是720P分辨率的码流。每个不同的分辨率都对应不同的码率。因此,也可以用来解决多人视频会议的问题,只是丢弃了高层次的层之后分辨率会变小。 但是我必须要说明一下,**H264、H265、VP8这些常用的编码标准(除了扩展)都是不支持空域SVC的。**因此,市面上的绝大多数的解码器也都不支持空域SVC这种一个码流里面含有多种分辨率的视频码流解码。所以现在很少会使用空域SVC,也很少有编码器实现空域SVC。并且,这种多分辨率的空域SVC相比多个编码器编码不同分辨率的方式,在压缩率上也没有多少优势,而且还不符合常规的标准。 因此,在WebRTC中直接使用多个编码器编码多种分辨率的方式代替空域SVC。 ![图片](https://static001.geekbang.org/resource/image/68/21/688080e0f0bbb7447e04231cd44ebc21.jpeg?wh=1920x621) 所以,我们接下来不会对空域SVC展开讨论。你可以当作是一个知识点了解一下就可以。 ## 时域SVC如何实现可伸缩 下面我们再来看一下时域SVC如何做到给不同带宽的接收端转发不同帧率和码率的视频流。当然这个只是我的一些经验之谈。你可以参考一下。 首先,我们需要一些字段来描述码流中当前帧的层号、帧序号等SVC信息。因为这些字段只有在编码器编码的时候才知道。我们需要在编码出来一帧之后,在RTP包里面打包上这些信息发送给服务器和接收端。为什么需要告诉服务器和接收端呢?我们先来讲讲服务器如何根据网络情况做分层转发策略。 一般来说,视频会议使用如下的架构做视频数据转发。 ![图片](https://static001.geekbang.org/resource/image/f7/c9/f74ae2e58c41e5fdba50d29a35096bc9.jpeg?wh=1920x1080) 服务器到接收端的链路上,服务器是发送端,在服务器上也需要做带宽预测,预测算法是一样的(可以参考[第10讲](https://time.geekbang.org/column/article/467073))。 服务器会预测得到每一个接收端和服务器之间链路的带宽值。发送端发送RTP包到服务器,服务器需要通过计算RTP包的大小和当前RTP包所属的帧属于哪一层得到每一层对应的码率。这样服务器在转发的时候,就可以根据到接收端之间链路的带宽值和对应的每一层的码率来选择到底转发几层。 比如说,视频的码率是2M,时域SVC编码,总共是3层,总帧率是24fps。第一层帧率是6fps,码率是500k;第二层帧率是6fps,码率是500k;第三层帧率是12fps,码率是1M(这里假设码率按帧数平均分配)。 假设某一个接收端只有600k,那服务器就只转发第一层给它,第二层第三层不转发。另一个接收端有1.5M,那我们就转发第一层和第二层给它,而第三层不转发。还有一个接收端是10M的带宽,我们就转发一二三层给它。这就是时域SVC的服务器转发逻辑。 这个有一个重要的点就是,服务器如何知道每一个RTP包对应帧所在的层号,以及接收端如何知道当前帧可不可以解码,因为接收端是不知道服务器到底给自己转发几层的码流的。 这里我们可以参考VP8编码的RTP协议标准。**VP8的RTP协议在RTP头和VP8码流数据的中间还有一个RTP描述头,这个描述头主要用来放帧号、层号等信息的。**具体如下图所示: ![图片](https://static001.geekbang.org/resource/image/62/fc/62908b385ab2637b92f80f67192005fc.jpeg?wh=1766x710 "图片来源VP8的RTP文档") 其中,几个重要的字段的解释如下: * I:占1位,表示有没有PictureID字段,为1表示有; * L:占1位,表示有没有TL0PICIDX字段,为1表示有; * T:占1位,表示有没有Tid和Y字段,为1表示有; * M:占1位,表示PictureID字段占7位还是15位,为1表示占15位; * PictureID:占7位或者15位,表示帧序号; * Tid:占2位,表示层号; * TL0PICIDX:占8位,表示当前帧所属的SVC单元,每过一个Tid为0的帧, TL0PICIDX加1; * Y:占1位,表示当前帧是不是只参考Tid=0的帧。 服务器可以从RTP描述头得到RTP包对应的层号。这样服务器就可以通过RTP的层号和RTP的包大小来估算每一层的码率了。 而接收端可以根据帧号、层号和层同步标志位等信息来判断当前帧是不是可以解码,而不用去解码视频码流。 ![图片](https://static001.geekbang.org/resource/image/a7/e6/a7d19637f8d089bd5b500d6b279ab3e6.jpeg?wh=1920x791) 从上图我们可以看到: * 帧0是IDR帧,只要完整了就一定可以解码; * 帧1是P帧,由于它的Y标志位为1,代表它只参考了同一个TL0PICIDX中Tid=0的帧,也就是帧0,因此,只要帧0可解,帧1就可以解码; * 帧2判断逻辑同帧1,只要帧0可解,帧2就可以解码,不依赖于帧1是不是可解; * 帧3也是P帧,但是由于它的Y=0,代表它不是只参考了Tid=0的帧,因此只有同一个TL0PICIDX中前面的帧都可解了才认为是可解的,也就是说只有帧0、帧1、帧2都可解它才可解,这里注意一下,因为帧3可以多参考,它可以同时参考帧1和帧2,只是图中没有画出来; * 帧4是P帧,但是它的Tid=0,因此它只参考前面的帧0,所以只要TL0PICIDX-1的Tid=0的帧可以解码,它就可以解码。也就是帧0可以解码,帧4就可以解码; * 对于帧5判断同帧1,帧6判断同帧2,帧7判断同帧3,一直循环下去; 我们可以看到帧1、帧3丢弃了的话,并不影响帧0和帧2的可解码性判断。帧1、帧2、帧3都丢失了,也不会影响帧4的可解码性的判断。因此,**我们的服务器就可以通过丢层的方式来实现对不同带宽的接收端下发不同帧率码率的码流了。** 上面是VP8的时域SVC的RTP协议。那H264呢?H264其实在标准的附录G直接定义了SVC的相关字段,也就是说在H264的编码码流里面就可以有SVC信息。如下图所示: ![图片](https://static001.geekbang.org/resource/image/80/eb/8051abb8d14e0a9f95abc81eb7c8e9eb.jpeg?wh=1920x928 "图片来源H264标准文档") 但是由于是附录G的内容,实现这一部分的解码器很少。因此不推荐使用这种方式传递SVC的相关信息。因为这种码流结构很多常规的H264解码器是不支持解码的,通用性不好,所以**我们建议使用RTP扩展头来传输时域SVC的信息。** 我们可以直接使用VP8的RTP描述头的格式,且编码码流还是保持常规标准的码流就可以,这样常规的H264解码器都能解码。服务器和接收端直接从RTP扩展头里面读取相关的SVC信息就可以了。而对于SVC编码,openh264已经实现了最大4层的时域SVC。你可以直接使用openh264就可以实现SVC编码了。 ## 小结 总结一下,今天我们通过多人视频会议如何设置编码码率的问题引出了为什么需要使用SVC编码。SVC编码可以在一个码流当中包含多个可以解码的子码流,这样服务器就可以根据接收端的带宽转发合适码率的子码流给接收端,从而达到可伸缩性。 并且,我们还介绍了两种类型的SVC,主要包括时域SVC和空域SVC。在之后,我们对服务器如何做时域SVC码流的转发做了详细的介绍。同时,我们还讨论了如何在RTP协议里面携带SVC信息,用于服务器做转发逻辑和接收端做解码性判断使用。 我们知道服务器会预测得到每一个接收端和服务器之间链路的带宽值,并通过计算RTP包的大小和当前RTP包携带的层号得到每一层对应的码率。然后,服务器再根据到接收端之间链路的带宽值和对应的每一层的码率来选择到底转发几层。 最后,接收端再根据RTP包携带的SVC信息来判断帧组完整之后可不可以解码,可以解码才能送解码器,不然就不能送去解码,防止出现花屏。这样我们就实现了可伸缩编码。 ## 思考题 通过前面的学习,你知道哪些弱网对抗手段? 欢迎你在留言区和我分享你的思考和疑惑,你也可以把今天所学分享给身边的朋友,邀请他加入探讨,共同进步。下节课再见。