当前位置: 首页 > news >正文

视频播放器选择怎样的丢帧策略~~

丢帧的出现

说起视频播放器大家都很熟悉了,覆盖各种平台,使用简单操作方面,但是视频播放器里面的原理却非常的复杂,牵扯到很多方面的知识点。

今天我们来探讨一下当视频解码和渲染的总时间大于了视频指定的时间时,就会出现声音比画面快的情况,单个画面延后的时间在人眼不能察觉的范围内还是能接受的,但是如此累计起来就会造成这个延迟的加大,导致后面声话完全不同步,这是不能接受的。

那么为了解决这种问题,视频“丢帧”就出现了。

视频播放原理

我们看到的视频其实就是一幅一幅的图片组成的,就和电影一样的原理,在很短的时间内连续把这些图片展示出来,这样就达到了视频连续的效果,比如每秒中展示25幅图片。

而在这25幅图片中某几幅(不能太多)图片没有展示出来,我们也是很难察觉的,这就是我们“丢帧”的基础了。

如果图片丢失多了,明眼人一眼就看出来了,那么就不用再讨论“丢帧”了,而是不会看你的这个视频了。

视频编码过程(H264)

现在视频编码比较流行的就是H264编码,它的压缩(编码)模式有很多种,适合于不同的场景,比如网络直播、本地文件、UDP传输等都会采样不同的压缩(编码)模式。

h264编码器会把一幅幅的图片压缩(编码)成体积很小的一个一个的单元(NALU),并且这些一个一个的单元之间并不是完全独立的。

比如:有10幅图片,经过编码后,第一幅图片会单独生成一个单元,而第二个图片编码后生成的单元只会包含和第一幅图片不同的信息(有可能第二幅图片和第一幅图片只有一个文字不一样,那么第二个单元编码后的数据就仅仅包含了这个文字的信息,这样的结果就是体积非常小),然后后面编码后的第三个、第四个单元一直到第十个单元都只包含和前一个单元或几个单元不同的信息(当然实际编码时很复杂的),这样的结果就是一个原本只有1G大小的一组图片编码后可能只有十多兆大小,大大减小了存储空间和传输数据量的大小。

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

前面提到的第一幅图片是被单独编码成一个单元(NALU)的,在H264中我们定义关键帧(用字母I表示,I帧,包含一幅图片的所有信息,可独立解码成一幅完整的图片)。

后面的第二个单元一直到第十个单元中的每一个单元我们定义为P帧(差别帧,因为它不包含完整的画面,只包含和前面帧的差别的信息,不能独立解码成一幅完整的图片,需要和前面解码后的图片一起才能解码出完整的图片)。

当然H264中还有B帧(双向帧,需要前后的数据才能解码成单独的图片),这就是我们经常听说的视频的I帧、P帧、B帧。

视频解码过程(H264)

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

请注意这里的一定的时间(这里的40ms)里面播放器需要做许多的事情:

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

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

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

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

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

大多数情况下,播放器所在设备的软硬件环境的解码能力都是可以让播放器在这个一定时间(比如40ms)内完成图片的显示的,这种情况下就是最好不过的了。

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

开始丢帧

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

比如我们的视频规定的是隔40ms(每秒25帧,且没秒的第一帧是I帧)显示一幅图片,而我们的设备解码能力有限,最快的解码出一幅图片的时间也需要42ms,这样本来该在40ms出显示第一幅图片,但是由于解码时间花了42ms,那么这一幅图片就在42ms时才显示出来。

比规定的时间(40ms)延迟了(42-40)2ms,当我们连续解码24幅图片时,这个延迟就到了20 * 2ms = 40ms,假设这个40ms的延迟已经很大了,再加大延迟就会造成我们明显感觉到视频的声音和画面不同步了。

所以我们就需要把后面的(25-24)1帧没解码的给丢了不显示(因为此时解码24帧的时间已经消耗了24*42=1008ms了,也就是说下一个40ms该显示第二秒的第一帧了,如果再显示第一秒的最后一帧,这样就会发生明显不同步的现象了),而是接着第二秒的数据开始解码显示,这样我们就成功的丢掉了一帧数据,来尽量保证我们的声音和画面同步了。

丢帧优化

前面提到的都是理想情况(每秒25帧,并且每一秒的第一帧都是I帧,能独立解码出图像,不依赖其他帧)下的丢帧,而不理想的情况(2个I帧直接的间隔不是定长的,比如第一个I帧和第二个I帧直接间隔24个其他帧,而第三个I帧和第二个I帧之间相差35个其他帧)则是经常遇到的。

这种情况下我们就不能写死解码播放24帧然后丢掉第25帧,因为可能出现丢掉25帧后的下一帧仍然不是I帧,这样解码就会解不出完整的图片,显示出来的画面就会有花屏,影响体验。

那么比较好的办法就是,我们定义一个内存缓冲区域,尽量在这个区域里面包含2个及以上的I帧(注意是解码前),比如:播放器从第一个关键帧开始解码播放,由于解码能力有限,当理论时间应该马上解码显示第二个关键帧时,而此时播放器还在解码这个关键帧之前的第5帧,也就是说播放器还得再解码5帧才能到这个关键帧,那么我们就可以把这5帧给丢掉了,不解码了,直接从这个关键帧开始解码,这样就能保证在每个关键帧解码播放时都和理论播放的时间几乎一致,让人察觉不到不同步现象,而还不会造成花屏的现象。

这种丢帧个人觉得才是比较不错的方案。

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;
 }
 //正常解码
 ...
}

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

cb2b3a5fcf2d6738770c1d5ca5098ebb.png

来源:https://blog.csdn.net/ywl5320/article/details/86514391

2d5afadab3864612a2b8867caf62a8f9.png

技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

6e2c02cd5f2fe84479894e4f6ca6fc22.png

私信领取相关资料

推荐阅读:

音视频开发工作经验分享 || 视频版

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

Android NDK 免费视频在线学习!!!

你想要的音视频开发资料库来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

87698ebad884827474274640705b0a64.gif

相关文章:

  • Window 下 FFmpeg 和 LibX264 的编译和配置
  • 智能语音技术新进展与发展趋势
  • Android 系统中的文字渲染~
  • 微博HDR视频的落地实践
  • 年末总结 | 音视频开发进阶 2021 干货合集
  • Shadertoy 详解
  • Shadertoy 进阶 01
  • 拍乐云首发音视频「分组讨论」开放能力,开启线上群聊互动新玩法
  • 浅入浅出WebGPU
  • Vulkan 在 FFmpeg 中的支持
  • 音视频中的语音信号处理技术
  • 声网3D空间音频技术解析:3D空间音效+空气衰减模拟+人声模糊
  • 音视频春节假期内卷指南(实操)
  • HDR技术趋势浅析
  • 干货收藏 || Vulkan Game Engine 视频教程
  • [译]Python中的类属性与实例属性的区别
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • 【面试系列】之二:关于js原型
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • php面试题 汇集2
  • Zepto.js源码学习之二
  • 今年的LC3大会没了?
  • 容器化应用: 在阿里云搭建多节点 Openshift 集群
  • 使用 QuickBI 搭建酷炫可视化分析
  • 使用agvtool更改app version/build
  • 小程序button引导用户授权
  • 协程
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (145)光线追踪距离场柔和阴影
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (附源码)spring boot北京冬奥会志愿者报名系统 毕业设计 150947
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (附源码)计算机毕业设计高校学生选课系统
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (十三)Maven插件解析运行机制
  • (转)【Hibernate总结系列】使用举例
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)ORM
  • ****Linux下Mysql的安装和配置
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?
  • .NET MVC第三章、三种传值方式
  • .NET 的静态构造函数是否线程安全?答案是肯定的!
  • .Net中的设计模式——Factory Method模式
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • .skip() 和 .only() 的使用
  • ?
  • @hook扩展分析
  • [20170705]diff比较执行结果的内容.txt
  • [ACL2022] Text Smoothing: 一种在文本分类任务上的数据增强方法
  • [Android Studio] 开发Java 程序