移动开发平台视频播放器探索
背景
APP开发中需要使用视频播放器播放视频,我们采用的是开源的第三方播放器IJK。虽然IJK功能强大,但是还是遇到不少问题。这里以iOS平台为例子记录下平时开发中学习到的知识和碰到的问题,以及如何解决。
视频播放基本流程
IJK是基于ffmpeg二次开发,其编解码依赖于ffmpeg和videoToolBox,视频显示采用的是SDL(基于openGL实现)。整个视频到画面的流程我简单用一张图概括下。
流程看上去简单,但是其中还涉及了多线程处理、图形学知识、音视频倍速播放等等知识,涉及的技术点很多,想学透还是比较困难的。下面我将挑几个基础的简单介绍。
知识点:视频解码I帧、B帧、P帧以及视频播放顺序
视频其实是一张张图片按照时间点连续显示到画面上而形成的。如果视频的清晰度很高,每张图片都很大,那么在网络上传输所付出代价就很大。所以视频都不是原图完完整整的保存的,而是需要进行"压缩",这个过程就是编码,而按照什么规则"压缩"就是编码格式,比如h265/h264。
在H.264压缩标准中,编码后,图片会变成I帧,B帧,P帧。
I帧
I帧其实就是完整的图片,其没有压缩,可以直接用来显示
P帧
P帧是基于前面的I帧进行编码的,所以要得到P帧原始的图片需要前面I帧或者P帧的数据。
B帧
B帧是基于前面的帧和后面的帧一起编码的,要叨叨B真原始图片需要前后2个帧。
视频播放顺序和视频解码顺序
由于B帧的存在,所以含有B帧视频的解码顺序和播放顺序是不一致的,比如
所以有B帧的视频播放顺序不等于解码顺序。
实际遇到问题1
前段时间遇到一个视频播放卡顿的问题,画面播放不连续。一开始以为是码率不够导致的跳帧,后来经过调试发现,音画同步并没有跳过帧,而是在排序队列出口有大量的跳帧。这个问题比较奇怪,于是我对该过程进行了模拟。
由于B帧的存在,解码的顺序不等于播放顺序,所以在解码出帧后需要将现有帧根据播放顺序排序,IJK的设计是将帧放入排序队列,并且保证队列里有2帧以上才允许出队列。那么上图播放流程如下图:
目前按正常思路来看是正常的,2队列深度不应该出现跳帧情况,但是事实调试确实出现了,这就比较奇怪了。后续查资料发现 B帧是依赖P帧和I帧解码,那会不会B帧不是按照队列先进先出,而是先进后出呢?图调整如下,不一样的地方已经标红:
是否规定B帧一定要先进先出这个没法考证,硬解码返回的顺序完全依赖VideoToolBox的代码实现,并且IJK有多年未维护,很可能是该问题导致。修改方案是将排序队列扩大,避免丢帧的情况出现。
知识点:RGB和YUV
RGB和YUV都是图像对颜色的编码方式,简单理解就是2中不同的方式来描述颜色,视频帧里面就是存储这些颜色信息。一个“屏幕”有许多像素点,帧数据就是告知“屏幕”中每一个像素点要显示什么颜色,然后所有像素点都显示自己的颜色后就形成了图片。
RGB就比较简单了,就是三原色,每个像素点都有红、绿、蓝个“灯泡”,像素点根据帧传进来的3个值来调整3个灯泡的亮度,光颜色叠加就能显示不同的颜色。
YUV是被欧洲电视系统所采用的一种颜色编码方法,之前都是黑白电视,RGB对黑白电视兼容性不好,YUV可以。Y”表示明亮度(Luminance或Luma),也就是灰阶值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。那现在显示屏是怎么用YUV显示的呢?其实很简单,就是将YUV根据算法转换成RGB,然后采用RGB的方式进行显示。
IJK播放器从帧转换成图像的过程简化如下:
图像显示这块讲述比较简单,具体可以参考其他资料,这里不再赘述。
实际问题2
近期在播放视频的时候发现视频播放画面显示异常,显示如下:
起初以为是解码问题,因为直接黑屏。但是仔细看,还有一些红色的影像在动。那么就先定位是什么问题导致。先通过电脑端测试视频,发现可以播放,说明视频本身没有问题。通过调试,发现可以正常解码出帧,并且音画同步和排序队列出帧扣都没有丢帧的情况。那么就可以初步定位问题出在帧->渲染这层。下面继续判断是解出的帧有问题还是渲染有问题。
videoToolBox解出的数据是CVImageBufferRef(可以参考IJKVideoToolBoxSync.m中VTDecoderCallback回调),我们可以通过代码,将CVImageBufferRef转换成UIimage再在页面上显示,看看帧是否能正常显示。结果可以显示,那么问题就出在渲染层了。
IJK的渲染是靠SDL,其核心还是openGL,又由于该视频是硬解码,所以直接定位到其render文件renderer_yuv420sp_vtb.m。下断点后发现,在openGL生成YUV Y层贴图时报错:
嗯,Y层贴图报错,是剩UV层,Y层是表示亮度,出错后显示黑色合情合理。但是该怎么修改呢,前面已经证明帧数据没有问题,那为什么Y层贴图会报错呢?并且这个错误也没有具体回调,线索就此断了,只知道是这里出问题,但是无法修改,尝试调整SDL几个参数无果,陷入僵局。
后来点击该方法,发现OPENGLES_DEPRECATED(ios(3.0, 12.0) 这么一句话,openGL在iOS12就被废弃了?通过查资料,原来openGL在iOS12之后就不建议使用,苹果采用替代方案Metal全面替代openGL,并且性能据说比之前高10倍。开发文档:开发文档 IJK渲染层只是将帧转化成图像,只要帧已经正确解出来并且格式固定,那么是采用OPENGL渲染还是采用Metal渲染对整个视频播放流程无影响。因此我们决定尝试采用Metal全面替换openGL。
经过几天尝试Metal,发现Metal的写法与OpenGL十分相似,而且采用OC编写面向对象,可以直接用ARC管理内存,省去了释放内存的烦恼。果然还是苹果自家的东西好用。。简单的思路是在IJK播放器盖一个MTKView,当使用Metal渲染时将MTKView解除隐藏,将解析出来的CVPixelBufferRef一层层传出来,递给MTKView显示。
优势:
1、CVPixelBufferRef-> MTKView都是苹果的类,兼容性好,不会像openGL出现异常报错
2、MTKView的性能更高
3、面向对象编程内存释放有保障
劣势:
1、有一定门槛,有openGL开发经验更容易理解。
调整后的视频如下: