使用常见的视频直播服务器,比如Nginx-RTMP做一个简单的Demo环境。我们用OBS往Nginx-RTMP上推流,随便找一个播放器播放。一般情况下,我们会看到一个黑屏,然后过一会之后,它才会出现图像。我们对比一下,如果我们看手机直播,像陌陌直播,映客,我们点一下播放按纽之后,一秒钟之内视频直接就出来了。今天我们要讲的内容就是这个差别是怎么形成的,背后的原理是什么。出现黑屏的原因很简单,就是解码器没有得到能解码出视频图像的数据,那么导致这个问题的原因就很容易定位了:
比较搞笑一点的说法就是你家的网络比较慢;
视频直播服务器没有打开关键帧缓冲。
那么就这些了吗?我想对于这个问题来说确实是这样。为了探究其中的原委,接下来我们需要往下去挖掘一下视频到底是怎么播放的。
视频是如何播放的
视频要播放它肯定是有视频数据,把视频数据放到编码器,然后编码器把这个视频数据解码出来,解成图片,然后播放到显示器上,这是一个基本的播放流程。一般来讲,大家现在主流的用H.264编码。对于H.264编码来说,我们会有三个不同的帧,所谓帧是什么呢?就是你看到的每一个图像。我们看到动态的视频,大家知道电影最开始用胶片拍的时候,每秒是25帧,是每秒25个图片在切换。对于H.264来讲,我们常见的有I帧,P帧,和B帧。
1.I帧,I-Frame也有人会叫Inter Frame,那么它的意义是什么?
(1)它是一个自描述帧,你可以理解为它就类似一个jpg图片,它里头所有的数据,你解出来之后,它就是一整张图片。
(2)无其他帧引用,它不需要去做前置和后置的引用。
(3)它压缩比是最小的,因为它要包括整个图片所有的数据在里头。
2.P帧,P-Frame也就是说预测帧,它的预测帧是怎么回事呢?大家有没有用过版本管理软件,比如git或SVN,这样可能大家会比较好理解,P帧就是保留变的部分,不变的部分你去上一个或者几个帧里面找就行。P帧只是负责向前引用,也就是任何一个P帧,它只看它往前的这些帧的数据。P帧的好处是什么呢?因为它只存一些变化信息,所以它大概的压缩比是I帧的50%。这个数据哪来的?大家可以去翻一下维基百科,那里会有一些介绍。
3.B帧,B-Frame,前后双向引用预测。
B帧比较特别,它要引用前面P帧某一部分的图像数据同时B帧后面的数据也会引用,这个是B帧的特点,它要引用前面的数据,也要引用后面的数据。那么它的优势就是压缩比比P帧还大,大概是I帧的25%,也就是我们B帧用的特别多的话,它会把视频的大小降的比较低,因为它的压缩比更大一些。
I帧,B帧,P帧它是怎么组成一个视频流呢?我们管这个东西叫Group Of Picture,简称叫GoP。
视频解码器,看到GoP它是怎么放呢?那很简单,编码器会有一个缓冲,然后它会保留从I帧开始,当然现在说是I帧,其实这个I帧还有个特殊的类型。从I帧开始,他会把数据缓存到解码器的Buffer里,当他遇到下一个P帧,或者再下一个B帧的时候,它会从它Buffer里找到它之前引用的那个帧,然后把这个数据解出来,最终播放到显示器上。那么缓冲区开始的第一个帧肯定是I帧,这个毋庸置疑。另外还有一个比较特别的点,B帧和P帧并不会只引用当前GoP里的帧,他可能会往前去引用上一个GoP中的帧。既然它有这么一个特性的话,我们什么时候去清空这个缓冲区呢?这里就要介绍一个新的概念,叫IDR帧。
IDR帧
IDR帧是I帧,但I帧并不一定是IDR帧,所谓IDR帧是什么?它就是拿到这个帧之后,播放器可以直接从这个帧开始往后播放,它保证后面的P帧和B帧的引用不会跨越这个IDR帧,那么看到IDR帧,编码器就可以把当前的Buffer清空,从当前这IDR帧开始解码往Buffer里边放,后续帧就可以从Buffer里的数据引用,然后解码,也就是说编码器可以从任何一个IDR帧开始解码。大家可以联想到,当我播放一个视频文件的时候,我可以拖动,但是我拖动的任何一个点,它肯定是一个IDR帧,当然它也是I帧,但是并不一定说每一个I帧我都能让它作为一个拖动的点。
IDR帧有时也有它不太学术的叫法:关键帧。在做编解码程序的时候,我们可能会看到FFmpeg的数据结构里会标着PTS和DTS,那么PTS和DTS是什么呢?
PTS和DTS是什么
PTS,Presentation Time Stamp也就说这个帧什么时候会放在显示器上;DTS就是Decode Time Stamp,就是说这个帧什么时候被放在编码器去解。那么如果全是I帧和P帧,PTS和DTS都是单调递增的,那么如果我们有B帧,会出现什么情况?因为大家都知道,对于B帧来讲,它会引用前面的帧和后面的帧。
我们看这个例子,就是当B帧进来的时候,因为它要引用后面的P帧,也要引用前面的I帧,可以看到DTS的顺序,一三四二,然后PTS顺序,一二三四。B帧它会根据它编码时候的特性,它会自动的把它的DTS时间戳往后挪,把它引用的帧先放到前面去,等它引用的帧解完了,数据解完了之后,才会把B帧去解,否则的话,我先把B帧放进去,它引用后面的P帧,P帧的数据还没有,B帧解不出来。所以说这点上给大家讲一下,B帧,P帧,I帧它们整个放在一个视频流里面,它的解码顺序和编码顺序,当然后续的话,我们可能会根据这个有一些开放性的思考。
为什么直播会等待
对于直播来讲,它是一个流,它不像点播,大家都从0秒开始,任何一个视频文件,0秒第一个帧肯定都是关键帧。那么对于直播来讲,我是一个随机的时间点接到这个视频流进行播放,那么我接入的这个时间点的帧有可能拿到的第一个帧的数据是I帧,也有可能是B帧,也有可能是P帧。这是一个随机的。在这种情况下,我们大概率会出现一个黑屏的状态。因为我拿到的是个P帧,对于P帧来讲,解码器面那个Buffer是空的,它不知道这个P帧如何进行解码,所以它只能丢弃这个帧。
对于直播来讲,我一秒钟的帧数是固定的,只能等到我下一个关键帧到来的时候,我才能开始去播放。当然正好赶巧了的话,接入那瞬间得到的数据正好是个I帧。就可以达到秒开的效果。
关键帧缓冲如何工作
其实是在cache服务器上,它会去预先解一下这个帧,然后去看它到底是个I帧,还是个B帧,还是个P帧,当它发现是I帧的时候,它会放在它的程序的内存里头,当你每一次打开这个视频流的时候,cache服务器会把内存中的I帧发送给客户端比如当前播放到了P帧,那我把P帧前面的I帧和P帧全波放到cache的内存里,然后当客户端接入之后先把内存里的数据发送给客户端解码器,然后再从这个B帧往后给。对于这个解码器来讲,它很舒服,它接到第一个数据流的第一个包肯定是I帧,那么它就可以直接播放了。
如何去做到秒开?
没有什么好的方法,任何人做这种事时候都是一个笨的方法,就是去看文档,没有什么捷径。大家可以翻阅FLV视频文件格式文档,它会告诉你package里头,它任何一个Video里的package有一个Video TagHeader,对它是有一个Frame Type,Frame Type如果把它解出来,它是1的时候,它管这个叫key Frame,咱们看后面这个AVC,a seekable Frame你可以理解为它是个IDR帧,它并不一定就是I帧。
几个问题
1.开放性的解决问题,GoP Cache是从当前的这个GoP帧开还是从上一个GoP开始
这个问题比较有意思的是我从上一个GoP放的话,我拿过来的肯定是直接可以放了。因为有时我也预见过一些比较特殊的编码,会导致我从当前这个GoP的第一个I帧拿播放器放出来,我这样可能会提高编码器的兼容性,但是它会有一个问题,就是如果GoP开的特别大的话,那么我的延时自然而然就会上去。因为我上一个GoP如果是十秒,等于说我拿的是十秒之前的数据,也就是你看到的是10秒之前他说的话,他做得表情,他做的动作。那么我从当前这个GoP开始,它肯定是有一定的延迟,但是不会大到超过你整个GoP。对于视频直播来讲,我们GoP size多少合适,换一句话讲如果GoP size设成0,所有都是I帧,不存在任何GoP cache问题,但是码率来讲会很高,因为所有的都是I帧,我们知道它压缩比会比较低,那么反过来,就是我需要设一个GoP的Size是多少呢?
2.视频直播的GoP Size设置成多少合适
一般来讲,对于手机直播,一到两秒可能是比较合适的,因为它本身的GoP时间也不会很长,我这边缓冲,一旦出现问题大概一到两秒这个视频也能出来。有一个不太好的地方就是它码率会稍微高一些,也就说同样的东西,如果我把GoP改成十秒,我可能是500K,但是我改成一秒,有可能变成一个六七百K的样子,这个还是跟编码有关系,具体的比例是多少,可能跟实际相关。
另外如果是点播的话,不关心首屏打开时间,只要是客户端下来速度快,CDN给力,那么我可能要求更小的范围。告诉大家一个实践过程中得出的结果,大家用过OBS?比如说做主播的话,大家用OBS会比较多,OBS它有一个问题就是它默认的话,如果你不调它的特性,GoP就是10秒,10秒的意思就是说GoP size。如果比较点背的话,看的是10秒之前的,如果是比较大的话,它的码率,码流的大小会小点,但是延迟会稍微高一些,CDN开了几个cache,有些情况下,我们也可以做些转码,强行把它的GoP size压小,整个CDN层面上加一个转码的话,它可能会增高这个延迟,这块一个开放性问题,大家可以根据自己的场景去思考,这个GoP Size配成多大比较合适。
3.你在视频直播中到底用不用B帧
我有时候在搜相关的资料,也有做直播的不用B帧,所以这一块我并没有什么结论,就是说给大家一个点,让大家去想一想。鉴于B帧之前看的DTS和PTS的PPT,也知道B帧在解码的时候,它是要打乱每一个帧传入解码器的顺序,如果丢包或者一些特殊情况,它可能会影响解码器的运行的特点。
一个数学故事
我不知道有多少人听说过这个事,就是1943年以前,二战的时候,因为英国被德国打的都已经找不着北了,他希望美国在后面支持英国,然后去做一些物资上的支持,那么就会有很多的商船,运输船把它的物资源源不断的从美国送到英国,德国人反制方式是什么?它的狼群潜艇在大西洋里逛。对于军事学家来讲,他们可能想不出来一些什么好的办法,他们就请来一些数学家问我这个商船怎么开比较合适,能保证一个是我的损失最小,另外一个我物资运送最大,碰到狼群的次数越低。这些数学家根据概率统计的概率论,然后得出一个结论,就是一定数量的船队编队规模越小,编次流越多与敌人相遇的概率就越大,换句话来讲,就是我这个船队肯定是攒足了,比如说我原来有一百艘船的货运量,那么我每一次发一艘船过去,那么我可能遇到狼群的概率会大一些,但是我把这一百艘船变成一个编队,然后我把我的护航编队做到足够好,我把我这一群送过去,我碰到的狼群概率很低的情况下,我可能效率更高一些。
数据包与网络抖动之间的斗争
我们从这个数学故事发散去考虑一个问题,我们把视频的数据保管好,运维的数据保管好,当成我们的运输舰,把网络走动,丢包,我们想成是一个狼群,就是德国的潜艇在整个大西洋上跑。我们就可以根据这个数学结论去考虑一个特殊的点,我们的发包节奏是什么样的。
因为我发一个包,比如说我发出一个数据包过去,你可以理解为,我从美国发了一批货运船,带了一行护行舰队去往英国,那么我们到底是有数据就往外发,还是我们攒一波数据之后往外发?这个就我刚才说的,你发包频率应该是有讲究的,看怎么去发比较合适。
另外,可不可以换一种思路,使用类似TCP慢起动这样的算法,我最开始为了保证首屏时间,我以最快的发包速率往上发,等我发到一定的程度的时候,换成慢包率发送,把时间拉长一点,拉倒一个可以接受的范围,然后逐渐的去调整这个东西,当然这可能是比较传统的,朴素的方式。最终的效果呢大家还得自己去根据这个特性自己想,这也是一个开放性的问题。谢谢大家。