要开始正儿八经地写视频系列文章了。思来想去,从播放器入手,再合适不过了。视频文件,只有播放出来,才显示出了意义;只有播放出来,才暴露出各种问题。先理解播放的场景,才能更好地理解视频处理时所选取的策略。
播放器播放视频,就是一步步剖开视频的内容,显示在屏幕上。
最简单的理解方式,是把视频文件看做一个容纳了很多图片的容器。播放时,从容器里取出一张图片,放到屏幕上显示,隔一点时间后,再从容器里取出下一张图,放到屏幕上。按次序把图片一张一张显示到屏幕上,等到最后一张也显示到屏幕上后,播放就完成了。
现在面临第一个问题:
隔多长时间去取下一张图呢? 人眼观看画面,限于视神经的反应速度,存在视觉暂留现象,其时值约是1/16秒,对于不同频率的光有不同的暂留时间。在暂留时间结束前,放入下一张图,人就感觉不出来是一张张的图,而是连续的动画了。在移动终端上观看的视频,每秒25帧图像,就很流畅了。一秒钟放的图像数,被称为帧率。
紧接着下个问题就来了:
一秒钟25帧图像,那么100秒的视频,容器里需要放置2500张图像,这是很大的数据量。无论存储还是传播,都是无法接受的。需要想办法减小数据量。从理论上分析,确实存在冗余信息,提供了压缩的可能性。而且,冗余信息还特别多,于是数据量可以大大地被压缩。
所以,视频容器里,放置的是压缩后的图像数据。那么播放器播放,就需要先解压缩成图像,再放到屏幕上。所以,播放器的两个核心功能,一个是解码,一个是显示。
我们来看看,Android为我们提供了哪些对象,可以让我们做视频的播放。
下面我们介绍3种在Android上播放视频的方法。
VideoView把解码和显示工作全部都封装起来,简单地设置视频路径,就可以进行播放了。 在显示方面,它就是一个View,可以在代码里创建,也可以在layout xml里直接定义。在解码方面,它支持常用的解码控制操作,如start(), pause(), resume(), seek(), seekTo()等。
看看它的内部实现,我们发现,解码使用了MediaPlayer,显示使用了SurfaceView。
那么,自己直接用SurfaceView和MediaPlayer,要怎么做?
Android系统,已经在底层我们打通了一条MediaPlayer到SurfaceView的数据通路,那就是Surface。 当SurfaceView被创建完成,就可以通过它的SurfaceHolder获取到它的Surface。把Surface传递给MediaPlayer,MediaPlayer解码的数据就会源源不断地输送到SurfaceView里。MediaPlayer有节奏地往Surface输入解码数据,SurfaceView会相应有节奏把Surface里的数据显示到屏幕上。
这种实现方式,解码和显示分别在两个对象中,可以分别控制。但是,我们无法控制它们的数据通路。要牢牢控制每一帧的数据,就要使用下面这种实现。
GLSurfaceView继承自SurfaceView,它实现了把opengl的渲染结果,绘制到给定的Surface里,进而可以显示在屏幕上。
它的几个主要特点:
让我们来看看,如何使用GLSurfaceView来实现视频的播放。
首先创建好GLSurfaceView。
setEGLContextClientVersion(2),指定EGLContext client version,2代表使用OpenGL ES 2.0。注意,它的调用必须要在setRenderer()之前。
setRenderer()指定用户自定义的renderer。这里指定为VideoRenderer,它通过实现GLSurfaceView.Renderer接口,控制了opengl渲染。
这个接口定义的三个方法,都执行在GLSurfaceView创建的gl线程中。
onSurfaceCreated()的调用发生在surface的创建或者重建时。gl线程的EGL context发生lost时,也会调用该方法。如手机从睡眠状态唤醒,会lost EGL context,此时onSurfaceCreated()方法会被调用。所以,渲染开始的资源申请和初始化工作,包括texture等资源的创建,都实现在这个方法中。 gl线程的EGL context发生lost后,和该context关联的所有opengl资源都会自动清除,使用者也无需专门去实现对应的glDelete*函数来清除已经lost的资源。
onSurfaceChanged()的调用发生在每次的onSurfaceCreated()之后,和每次surface size发生改变时。官方推荐在此处做投影和视口变换,但是,通常情形下,不会发生size变化,所以为了简化实现,往往保持该方法为空。
onDrawFrame()的调用发生在绘制当前帧时。每一次要显示的内容,都在这个方法里完成opengl渲染。
下面我们来看具体如何定义VideoRenderer,来实现视频播放。
在onSurfaceCreated()里做了三件事:
1)initProgram()
创建和编译好opengl shader program,用于把图像数据渲染出来。
2)initSurfaceTexture()
为视频解码器MediaPlayer和opengl对象texture的连接,创建数据通路。
把opengl的一个texture,封装到SurfaceTexture中。为该SurfaceTexture设置数据获取的回调onFrameAvailableListener。当SurfaceTexture获取到数据,该回调就会被执行。
那么,如何往SurfaceTexture里放入数据呢?接着看下面的实现。
3)mediaPlay()
把SurfaceTexture封装在Surface对象中,赋给MediaPlayer。MediaPlayer就会把解码数据源源不断地放入SurfaceTexture中了。
放入到SurfaceTexture中的数据,我们要如何来使用呢?
需要把数据从SurfaceTexture中取出来,放到opengl texture中。实现如下:
SurfaceTexture的updateTexImage()方法,把SurfaceTexture中的图像图像数据取到opengl texture中。getTransformMatrix()告诉opengl需要对该图像做一个基本的变换,通常为上下翻转。
至此,opengl拿到了解码的图像数据后,就可以自如的做任何图像相关的处理,渲染到屏幕上。
以上在Android上实现的三种播放视频方法,从简单到复杂,可以根据自己功能的需要,灵活进行选择。如果只是简单地播放视频,可以使用VideoView。如果对播放有更多的控制需求,可以使用MediaPlayer和SurfaceView。如果要对每一帧图像做处理,可以使用MediaPlayer和GLSurfaceView。
作者简介:taoxiong(熊涛),天天P图Android工程师