Android中如何使用OpenGL播放视频

2020-06-23 16:19:14 浏览数 (1)

视频播放流程

视频播放主要经历这么几个步骤:解协议 -> 解封装 -> 解码音视频 -> 音视频同步,流程如下图:

其中播放网络视频才需要解协议,直接播放本地视频是不需要这一步的

解协议:将流媒体协议的数据解析为相应标准的封装格式数据。音视频在网络上进行传播的时候,通常会采用各种流媒体协议,如HTTP,RTMP等,这些协议在传输音视频数据的同时会增加一些信令信息(播放状态,网络状态描述等)。解协议的过程中会除掉信令数据而只保留音视频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,会输出FLV格式的数据

解封装:将输入的封装格式数据分离为音频流压缩编码数据和视频流压缩编码数据。封装格式有很多,常见的如MP3,MP4,FVL,AVI等

解码:将音频/视频压缩编码数据解码为非压缩的音频/视频原始数据。常见的音频压缩编码标准包括AAC,MP3等,视频压缩编码标准包括H.264, H.265(支持4k,8k视频), MEPG2等。解码是整个流程中最核心和最复杂的一步,通过解码,压缩编码的音频数据解压为非压缩的音频抽样数据,如PCM;压缩编码的视频数据解压为非压缩的颜色数据,如YUV,RGB等

音视频同步:通过解封装步骤中获取的相关参数,同步解码出来的视频和音频数据,并发送到系统的显卡和声卡中进行播放

MediaPlayer生命周期

Android系统中,播放视频可以使用MediaPlayer来完成上面的播放流程,常用的VideoView控件内部也是封装了MediaPlayer

MediaPlayer生命周期如下图:

MediaPlayer的使用必须遵循节点之间的状态转换,不然很容易出现IllegalStateException异常

MediaPlayer的使用

MediaPlayer的构造分为两种:

第一种是直接new一个新对象,new出来的对象处于Idle状态;

第二种是通过静态方法create创建的对象,该对象直接处于prepare状态,源码如下:

使用MediaPlayer的时候,需要一个surface来消费数据,我们可以使用SurfaceView或TextureView

使用SurfaceView的时候,绑定到SurfaceHolder即可

代码语言:javascript复制
override fun surfaceCreated(holder: SurfaceHolder?) {
     mediaPlayer.setDisplay(holder)
}

使用TextureView的时候,则利用TextureView持有的SurfaceTexture创建一个Surface并设置给MediaPlayer

代码语言:javascript复制
val surface = Surface(textureView.surfaceTexture)
mediaPlayer.setSurface(surface)

OpenGL播放视频

当我们需要利用OpenGL播放视频的时候,可以使用MediaPlayer GLSurfaceView的组合,因为GLSurfaceView已经创建好了EGL环境,方便快速引入

整个流程的核心在于 setSurface 这个接口,我们利用一个OES纹理生成SurfaceTexture,然后利用这个SurfaceTexture生成Surface并设置给MediaPlayer,这样当每解一帧视频数据的时候,就将视频颜色数据更新到OES纹理中,然后利用GL绘制到屏幕即可

代码语言:javascript复制
oesTextureId = TextureHelper.createOESTextureObject()
surfaceTexture = SurfaceTexture(oesTextureId)
val surface = Surface(surfaceTexture)
mediaPlayer.setSurface(surface)

更新播放进度

代码语言:javascript复制
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    glSurfaceView.requestRender()
    val position = mediaPlayer.currentPosition
    videoSeekBar.progress = position
    playTimeView.text = calculateTime(position)
}

默认情况下,绘制的视频会填满整个GLSurfaceView的大小,通常都会有视频播放拉伸的问题,因为视频的宽高比和容器的宽高比不一致,我们可以通过设置模型矩阵来调整GL顶点

代码语言:javascript复制
override fun onVideoSizeChanged(mp: MediaPlayer?, width: Int, height: Int) {
   videoSeekBar.max = mediaPlayer.duration
   sx = 1f * width / renderWidth
   sy = 1f * height / renderHeight
}

override fun onDrawFrame(gl: GL10?) {
    surfaceTexture?.updateTexImage()
    surfaceTexture?.getTransformMatrix(transform)
    
    Matrix.setIdentityM(mvpMatrix, 0)
    Matrix.scaleM(mvpMatrix, 0, sx, sy, 1f)
    cameraRender?.drawTexture(mvpMatrix, transform, oesTextureId)
}

MediaPlayer解出的视频帧转为纹理后,我们就可以通过GL操作纹理做很多有趣的东西了,比如添加水印,添加滤镜效果等,这里简单的将rgb转为gray

代码语言:javascript复制
Gray = 0.2989 * R   0.5870 * G   0.1140 * B

完整的顶点着色器

代码语言:javascript复制
attribute vec4 a_Position;
attribute vec4 a_TextureCoordinate;

uniform mat4 u_TextureMatrix;
uniform mat4 u_MvpMatrix;

varying vec2 vTextureCoord;

void main() {
    vTextureCoord = (u_TextureMatrix * a_TextureCoordinate).xy;
    gl_Position = a_Position * u_MvpMatrix;
}

片元着色器

代码语言:javascript复制
#extension GL_OES_EGL_image_external:require
precision mediump float;

uniform samplerExternalOES u_TextureSampler;
uniform float identity;

varying vec2 vTextureCoord;

void main() {
    vec4 color = texture2D(u_TextureSampler, vTextureCoord);
    float gray = 0.299 * color.r   0.587 * color.g   0.114 * color.b;
    vec4 newColor = vec4(gray, gray, gray, color.a);
    gl_FragColor = mix(color, newColor, identity);
}

DEMO

Demo中,在点击播放按钮的时候才开始更新GLSurfaceView,为了避免启动后界面一片黑,我们在GLSurfaceView上盖一层ImageView来展示一帧图像,开始播放后就隐藏这个ImageView,获取视频缩略图:

代码语言:javascript复制
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(
       assetFileDescriptor.fileDescriptor,
       assetFileDescriptor.startOffset,
       assetFileDescriptor.length
)
val bitmap = mediaMetadataRetriever.getFrameAtTime(0)
if (bitmap != null) {
     videoThumbnailView.visibility = View.VISIBLE
     videoThumbnailView.setImageBitmap(bitmap)
}
mediaMetadataRetriever.release()

Demo截图:

传送门:

https://github.com/sifutang/video.git

0 人点赞