视频播放器丢帧策略

我的视频课程(基础):《(NDK)FFmpeg打造Android万能音频播放器》

我的视频课程(进阶):《(NDK)FFmpeg打造Android视频播放器》

我的视频课程(编码直播推流):《Android视频编码和直播推流》

我的视频课程(C++ OpenGL):《Android C++ OpenGL教程》

 

1、丢帧的出现

说起视频播放器大家都很熟悉了,覆盖各种平台,使用简单操作方面,但是视频播放器里面的原理却非常的复杂,牵扯到很多方面的知识点。今天我们来探讨一下当视频解码和渲染的总时间大于了视频指定的时间时,就会出现声音比画面快的情况,单个画面延后的时间在人眼不能察觉的范围内还是能接受的,但是如此累计起来就会造成这个延迟的加大,导致后面声话完全不同步,这是不能接受的,那么为了解决这种问题,视频“丢帧”就出现了。

2、视频播放原理

我们看到的视频其实就是一幅一幅的图片组成的,就和电影一样的原理,在很短的时间内连续把这些图片展示出来,这样就达到了视频连续的效果,比如每秒中展示25幅图片。而在这25幅图片中某几幅(不能太多)图片没有展示出来,我们也是很难察觉的,这就是我们“丢帧”的基础了。如果图片丢失多了,明眼人一眼就看出来了,那么就不用再讨论“丢帧”了,而是不会看你的这个视频了。

3、视频编码过程(H264)

现在视频编码比较流行的就是H264编码,它的压缩(编码)模式有很多种,适合于不同的场景,比如网络直播、本地文件、UDP传输等都会采样不同的压缩(编码)模式,h264编码器会把一幅幅的图片压缩(编码)成体积很小的一个一个的单元(NALU),并且这些一个一个的单元之间并不是完全独立的,比如:有10幅图片,经过编码后,第一幅图片会单独生成一个单元,而第二个图片编码后生成的单元只会包含和第一幅图片不同的信息(有可能第二幅图片和第一幅图片只有一个文字不一样,那么第二个单元编码后的数据就仅仅包含了这个文字的信息,这样的结果就是体积非常小),然后后面编码后的第三个、第四个单元一直到第十个单元都只包含和前一个单元或几个单元不同的信息(当然实际编码时很复杂的),这样的结果就是一个原本只有1G大小的一组图片编码后可能只有十多兆大小,大大减小了存储空间和传输数据量的大小。

4、H264中 I帧、P帧、B帧的含义

前面提到的第一幅图片是被单独编码成一个单元(NALU)的,在H264中我们定义关键帧(用字母I表示,I帧,包含一幅图片的所有信息,可独立解码成一幅完整的图片),后面的第二个单元一直到第十个单元中的每一个单元我们定义为P帧(差别帧,因为它不包含完整的画面,只包含和前面帧的差别的信息,不能独立解码成一幅完整的图片,需要和前面解码后的图片一起才能解码出完整的图片),当然H264中还有B帧(双向帧,需要前后的数据才能解码成单独的图片),这就是我们经常听说的视频的I帧、P帧、B帧。

5、视频解码过程(H264)

通过前面的讲解,相信大家对视频编码后图片的变化过程有了大概的了解了(了解过程就行,具体技术细节就不用追究了),那么我们的重点就来了,播放器播放视频的过程就和图片编码成视频单元(NALU)的过程相反,而是把我们编码后的I帧、P帧、B帧中的信息解码后,依照编码顺序还原出原来的图片,并按照一定的时间显示(比如每秒显示25幅图片,那么每幅图片之间的间隔就是40ms,也就是每隔40ms显示一幅图片)。请注意这里的一定的时间(这里的40ms)里面播放器需要做许多的事情:

1、读取视频文件或网络数据

2、识别读取的数据中的视频相关的数据

3、解析出里面的每一个单元(NALU),即每一帧(I、P、B)

4、然后把这些帧解码出完整的图片(I帧可以解码成完整图片,P、B帧则不可以,需要参考其他帧的数据)

5、最后按照一定的时间间隔把解码出来的图片显示出来

大多数情况下,播放器所在设备的软硬件环境的解码能力都是可以让播放器在这个一定时间(比如40ms)内完成图片的显示的,这种情况下就是最好不过的了。而也有设备软硬件环境的解码能力不能在这个一定时间(比如40ms)内完成图片的显示,但是呢又相差不大(比如相差几毫秒),但是随着解码的次数增加,这个时间就会累计,后面就有可能相差几秒、几十秒、几分钟等,这样就需要“丢帧”操作了。

6、开始丢帧

丢帧丢帧,怎么丢,丢掉哪些帧我们怎么决定呢,这就要从视频图像是怎么解码得到的原理下手了,不然随便丢帧的话,最容易出现的情况就是花屏,导致视频基本不能看。下面我就举个例子来说明怎样丢帧:

比如我们的视频规定的是隔40ms(每秒25帧,且没秒的第一帧是I帧)显示一幅图片,而我们的设备解码能力有限,最快的解码出一幅图片的时间也需要42ms,这样本来该在40ms出显示第一幅图片,但是由于解码时间花了42ms,那么这一幅图片就在42ms时才显示出来,比规定的时间(40ms)延迟了(42-40)2ms,当我们连续解码24幅图片时,这个延迟就到了20 * 2ms = 40ms,假设这个40ms的延迟已经很大了,再加大延迟就会造成我们明显感觉到视频的声音和画面不同步了,所以我们就需要把后面的(25-24)1帧没解码的给丢了不显示(因为此时解码24帧的时间已经消耗了24*42=1008ms了,也就是说下一个40ms该显示第二秒的第一帧了,如果再显示第一秒的最后一帧,这样就会发生明显不同步的现象了),而是接着第二秒的数据开始解码显示,这样我们就成功的丢掉了一帧数据,来尽量保证我们的声音和画面同步了。

7、丢帧优化

前面提到的都是理想情况(每秒25帧,并且每一秒的第一帧都是I帧,能独立解码出图像,不依赖其他帧)下的丢帧,而不理想的情况(2个I帧直接的间隔不是定长的,比如第一个I帧和第二个I帧直接间隔24个其他帧,而第三个I帧和第二个I帧之间相差35个其他帧)则是经常遇到的,这种情况下我们就不能写死解码播放24帧然后丢掉第25帧,因为可能出现丢掉25帧后的下一帧仍然不是I帧,这样解码就会解不出完整的图片,显示出来的画面就会有花屏,影响体验。那么比较好的办法就是,我们定义一个内存缓冲区域,尽量在这个区域里面包含2个及以上的I帧(注意是解码前),比如:播放器从第一个关键帧开始解码播放,由于解码能力有限,当理论时间应该马上解码显示第二个关键帧时,而此时播放器还在解码这个关键帧之前的第5帧,也就是说播放器还得再解码5帧才能到这个关键帧,那么我们就可以把这5帧给丢掉了,不解码了,直接从这个关键帧开始解码,这样就能保证在每个关键帧解码播放时都和理论播放的时间几乎一致,让人察觉不到不同步现象,而还不会造成花屏的现象。这种丢帧个人觉得才是比较不错的方案。

8、FFMpeg解码伪代码

bool dropPacket = false;
while(true)
{
	AVPacket *pkt = getVideoPacket();
	if(audioClock >= lastKeyFrameClock + offsetTime)//当前音频时间已经超过了下一帧关键帧之前了,就需要丢帧了
	{
		dropPacket = true;
	}
	if(pkt->flags == KEY_FRAME)//关键帧不丢
	{
		dropPacket = false;
	}
	if(dropPacket)
	{
		av_packet_free(pkt);
		av_free(&pkt);
		pkt = NULL;
		continue;
	}
	//正常解码
	...
}

最后来一张出自灵魂画手的丢帧图:

视频播放器丢帧策略_第1张图片

 

你可能感兴趣的:(视频教程)