课程介绍

在学习了前面章节OpenGL基础知识后,读者应该具备了复杂界面特效、图片高效处理的开发能力。接下来的章节主要转向Android视频应用开发中的OpenGL ES部分。

一. 视频播放器搭建

1. 视图容器

界面视图容器依旧使用GLSurfaceView,绘制方式是RENDERMODE_CONTINUOUSLY持续绘制的模式(课程演示,减少框架部分,相应的有不必要的性能损耗)。

2. 必要框架

因为本节涉及外部文件读取,所以会涉及到外部存储读写权限的获取、文件URI的解析、媒体文件数据解析,该部分内容非本节重点,因此详情见工程代码。

3. 媒体播放器

要驱动视频进行播放,需要借助到系统的媒体播放器MediaPlayer,它的相关方法如下:

  • setDataSource:设置数据源,这里直接传入文件路径
  • isLooping:是否循环播放
  • prepare:进入准备状态
  • start:开始播放
  • pause:暂停
  • stop:停止
  • release:释放资源

具体的API可以参见官网,生命周期流程图如下:

mediaplayer_state_diagram

播放器生命周期的逻辑处理,详见工程代码。这里重点讲解下如何将MediaPlayer和GLSurfaceView进行绑定使用,从而可以在GL上进行视频渲染播放。

①. 绑定纹理ID

//  1. 创建纹理IDval textureIds = ......
//  2. 创建SurfaceTexture、Surface,并绑定到MediaPlayer上,接收画面驱动回调surfaceTexture = SurfaceTexture(textureIds[0])surfaceTexture!!.setOnFrameAvailableListener(this)val surface = Surface(surfaceTexture)mediaPlayer.setSurface(surface)

这里分为以下几步:

  1. 创建纹理ID,用于GL渲染
  2. 通过纹理ID创建一个SurfaceTexture对象
  3. 通过SurfaceTexture对象创建一个Surface对象
  4. 将Surface传入MediaPlayer中

通过以上4步,将纹理ID和MediaPlayer进行关联。

②. 接收画面解析完毕的回调

当视频帧解析完毕时,MediaPlayer会通过层层的接口调用到SurfaceTexture的onFrameAvailable接口,这时候我们可以标志下画面已经解析完毕。

/*** MediaPlayer有新的画面帧刷新时,通过SurfaceTexture的onFrameAvailable接口进行回调*/
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {updateSurface = true
}

③. 驱动画面更新纹理ID

override fun onDrawFrame(glUnused: GL10) {if (updateSurface) {// 当有画面帧解析完毕时,驱动SurfaceTexture更新纹理ID到最近一帧解析完的画面,并且驱动底层去解析下一帧画面surfaceTexture!!.updateTexImage()updateSurface = false}// ...之后通过纹理ID绘制画面
}

在这里必须将updateTexImage放在onDrawFrame中进行调用,而不能放在第二步的onFrameAvailable方法中,因为这个方法内部介绍了,必须是在GL线程中进行调用,而不能在主线程中。而GL线程的生命周期方法有onSurfaceCreated、onSurfaceChanged、onDrawFrame,想要不停地更新画面,自然是放在onDrawFrame中最为合适。

二. GL渲染视频画面

之前课程中,我们渲染的纹理都是2D纹理,而在纹理绘制的课程中,有介绍到纹理单元是包括多种类型的纹理目标,包括了GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP、GL_TEXTURE_OES,而本节课用到了GL_TEXTURE_OES这个类型的纹理目标。

要启用GL_TEXTURE_OES这种拓展类型的纹理目标,需要对之前课程中的2D纹理进行以下修改。

1. 修改纹理类型声明

private const val FRAGMENT_SHADER = """#extension GL_OES_EGL_image_external : requireprecision mediump float;varying vec2 v_TexCoord;uniform samplerExternalOES u_TextureUnit;void main() {gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);}"""

在编写Shader时,需要声明当前使用的是拓展纹理GL_OES_EGL_image_external,然后采样器类型必须是samplerExternalOES。

另外经过测试,在Fragment Shader中,不能同时启用2D纹理和OES纹理的绘制,即使只是声明而不执行也是不可以的。

2. 创建纹理、绘制纹理

创建纹理、绘制纹理时,我们绑定的纹理目标类型必须改为是OES类型的。

GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId)

在完成这一步时,应该就可以进行视频画面的播放了,不过还会有些小问题,比如视频比例、方向的处理,下面会为大家解决这两个问题。


三. 解决视频角度问题

手机相机拍摄出来的文件,若带了Exif文件信息,那么就需要对文件解析展示的过程中进行处理。否则就会出现和我们预期不一致的效果。

可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。

角度问题效果如下图:

视频播放器方向问题

视频Exif信息包含了视频的方向,通过以下方法可以获取到。

/*** 初始化视频信息*/
fun initMetadata() {if (TextUtils.isEmpty(path)) {return}try {val retriever = MediaMetadataRetriever()retriever.setDataSource(path)degrees = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_ROTATION))duration = getInteger(retriever.extractMetadata(METADATA_KEY_DURATION))bitRate = getInteger(retriever.extractMetadata(METADATA_KEY_BITRATE))width = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_WIDTH))height = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_HEIGHT))displayWidth = if (isDisplayRotate) height else widthdisplayHeight = if (isDisplayRotate) width else heightretriever.release()} catch (e: Exception) {e.printStackTrace()}
}private fun getInteger(value: String): Int {return if (TextUtils.isEmpty(value)) 0 else Integer.valueOf(value)
}

这里获取到的Degree信息就是视频的角度信息,这里返回的Int值有4个可能:0、90、180、270,这代表了视频若想要正确播放,那么需要顺时针方向旋转这个角度值。

想要解决视频方向问题,可以通过旋转顶点坐标旋转纹理坐标的方式去实现,这里我采用的是旋转顶点坐标的方式,工具类如下:

/*** 顶点旋转工具类** @author Benhero* @date   2019/2/11*/
object VertexRotationUtil {enum class Rotation {NORMAL, ROTATION_90, ROTATION_180, ROTATION_270;}fun getRotation(rotation: Int): Rotation {return when (rotation) {0 -> VertexRotationUtil.Rotation.NORMAL90 -> VertexRotationUtil.Rotation.ROTATION_90180 -> VertexRotationUtil.Rotation.ROTATION_180270 -> VertexRotationUtil.Rotation.ROTATION_270else -> VertexRotationUtil.Rotation.NORMAL}}fun rotate(rotation: Int, srcArray: FloatArray): FloatArray {return VertexRotationUtil.rotate(getRotation(rotation), srcArray)}fun rotate(rotation: VertexRotationUtil.Rotation, srcArray: FloatArray): FloatArray {return when (rotation) {VertexRotationUtil.Rotation.ROTATION_90 -> floatArrayOf(srcArray[2], srcArray[3],srcArray[4], srcArray[5],srcArray[6], srcArray[7],srcArray[0], srcArray[1])VertexRotationUtil.Rotation.ROTATION_180 -> floatArrayOf(srcArray[4], srcArray[5],srcArray[6], srcArray[7],srcArray[0], srcArray[1],srcArray[2], srcArray[3])VertexRotationUtil.Rotation.ROTATION_270 -> floatArrayOf(srcArray[6], srcArray[7],srcArray[0], srcArray[1],srcArray[2], srcArray[3],srcArray[4], srcArray[5])else -> srcArray}}
}

问题解决后效果如下图:

视频播放器方向问题解决

四. 解决视频比例问题

在播放横向视频时,可能会遇到下面这种情况,这是由于我们之前视频的容器都是充满屏幕的,而屏幕比例和视频的比例不一致,所以才会有拉伸的问题。所以只要调整视频容器的方向和视频比例一致即可。

问题现象如下图:

视频比例问题.png

这里我们借助一个自定义View来解决这个比例问题,只要将这个View作为GLSurfaceView的父容器即可。

/*** 自动适配比例的布局*/
class AspectFrameLayout : FrameLayout {private var mTargetAspect = -1.0/*** 是否自动适配尺寸*/private var mIsAutoFit = trueconstructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet) : super(context, attrs)fun setAspectRatio(aspectRatio: Double) {if (aspectRatio < 0) {throw IllegalArgumentException()}Log.w(TAG, "Setting aspect ratio to $aspectRatio (was $mTargetAspect)")if (mTargetAspect != aspectRatio) {mTargetAspect = aspectRatiorequestLayout()}}fun setAutoFit(autoFit: Boolean) {mIsAutoFit = autoFit}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {var widthMeasure = widthMeasureSpecvar heightMeasure = heightMeasureSpecif (!mIsAutoFit) {super.onMeasure(widthMeasure, heightMeasure)return}if (mTargetAspect > 0) {var initialWidth = View.MeasureSpec.getSize(widthMeasure)var initialHeight = View.MeasureSpec.getSize(heightMeasure)val horizontalPadding = paddingLeft + paddingRightval verticalPadding = paddingTop + paddingBottominitialWidth -= horizontalPaddinginitialHeight -= verticalPaddingval viewAspectRatio = initialWidth.toDouble() / initialHeightval aspectDiff = mTargetAspect / viewAspectRatio - 1if (Math.abs(aspectDiff) < 0.01) {Log.w(TAG, "aspect ratio is good (target=" + mTargetAspect +", view=" + initialWidth + "x" + initialHeight + ")")} else {if (aspectDiff > 0) {initialHeight = (initialWidth / mTargetAspect).toInt()} else {initialWidth = (initialHeight * mTargetAspect).toInt()}initialWidth += horizontalPaddinginitialHeight += verticalPaddingwidthMeasure = View.MeasureSpec.makeMeasureSpec(initialWidth, View.MeasureSpec.EXACTLY)heightMeasure = View.MeasureSpec.makeMeasureSpec(initialHeight, View.MeasureSpec.EXACTLY)}}super.onMeasure(widthMeasure, heightMeasure)}companion object {private const val TAG = "AspectFrameLayout"}
}

最后只需要在解析到视频文件信息,读取到视频的比例后,通过setAspectRatio将比例值设置进这个自定义View,就可以解决比例问题。

解决效果如下图

视频比例问题解决

对于这个纵向问题的解决方案,可能有些读者会疑惑,为什么不通过OpenGL自身来解决呢?如果通过OpenGL来解决,确实是可以通过按照比例修改顶点坐标参数。但是,这样的话,会引起几个问题:

  1. 视频内容外区域的颜色: 由于背景色会受GLES20.glClear影响,或者如果我们在当前画面添加了一个反色滤镜,那么示例图中的黑色部分就会变成白色,也就是说这块不属于视频内容的区域,被GL所控制,这应该不是我们想看到的产品效果;
  2. 水印坐标:如果想要在视频画面上添加内容,那么我们定了水印的坐标在视频的右下角,那么如果传进来的值是(1.0 - 水印宽度, -1.0 - 水印高度),那么我们会看到的效果是,水印添加到了手机的右下角,而不是视频内容区域的右下角,这也不是我们想看到的,这会导致我们需要对之后所有绘制在视频上的图层做坐标换算处理,带来很多工作量。

Android OpenGL ES (十三) 视频播放器相关推荐

  1. 利用 OpenGL ES 给视频播放器和相机做个字符画滤镜

    该原创文章首发于微信公众号:字节流动 最后不少朋友问,"OpenGL ES 入门后怎么学习写一些滤镜?","怎么学习 shader ?". 最近请教了一些大佬, ...

  2. Android OpenGl Es 学习(二):定义顶点和着色器

    概述 这是一个新的系列,学习OpengGl Es,其实是<OpenGl Es 应用开发实践指南 Android卷>的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为 ...

  3. Android OpenGL ES视频渲染(一)GLSurfaceView

    相关文章:Android OpenGL ES视频渲染(二)EGL+OpenGL Android中视频渲染有几种方式,之前的文章使用的是nativewindow(包括softwareRender).今天 ...

  4. Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  5. Android OpenGL ES 从入门到精通系统性学习教程

    1 为什么要写这个教程 目前这个 OpenGL ES 极简教程的更新暂时告一段落,在此之前,很荣幸获得了阮一峰老师的推荐. 因为在工作中频繁使用 OpenGL ES 做一些特效.滤镜之类的效果,加上平 ...

  6. Android OpenGL ES 学习(二) -- 图形渲染管线和GLSL

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  7. Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  8. Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  9. Android OpenGL ES 学习(九) – 坐标系统和实现3D效果

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  10. Android OpenGL ES 学习(五) -- 渐变色

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

最新文章

  1. # Ubuntu 配置自带vnc桌面共享
  2. Maven多工程的创建
  3. Open×××的新钩子设计
  4. java swt webkit_使用Java和Webkit的HTML编辑器 – SWT浏览器
  5. Swift5版本以上自定义回调函数报错
  6. sqlite3_column
  7. 苹果iOS 13概念曝光 音量UI大改鼠标即时配对
  8. vue之elementui表单验证最基本实例
  9. 数据结构与算法之递推算法 C++与PHP实现
  10. 差分进化算法python_L单目标差分进化算法
  11. Python入门之函数调用
  12. 【性能优化】记录一次YounGC峰值优化
  13. 【学习笔记】图像纹理特征
  14. 编译原理——设计一个词法分析程序,实现对C程序设计语言的源程序(自定)的词法分析
  15. 关于工作后定居城市的思考以及房价预测(天涯原创)
  16. 剪不断理还乱的前端和交互
  17. mysql优化之 Using where; Using join buffer (Block Nested Loop) ,索引失效,检查项
  18. JAVA分布式快速开发基础平台 iBase4J 推荐 国产 J2EE框架
  19. Nodejs纯esm模块的迁移方法、社区冲击、评价浅论
  20. 网络上的两台计算机如何互相访问,win10如何实现两台电脑无线互访

热门文章

  1. 小码农的职场人生一:由张小平离职引发的一些吐槽
  2. 推荐阅读:太极拳的奥妙-专访七十肖维佳老翁现场展示
  3. python错误提示库没有注册_无法访问计算机XXXX。错误是:库没有注册
  4. DY-SV17F语音播放模块应用篇一 【IO独立模式】
  5. 小白玩大数据日志分析系统经典入门实操篇FileBeat+ElasticSearch+Kibana 实时日志系统搭建从入门到放弃
  6. 【影像组学】CT数据与MRI数据
  7. Spark - SizeEstimator.estimate 字节估算之时间都去哪了
  8. python每个数都可由集合中的某两个数相加而得
  9. 定时任务多线程-springboot
  10. js:bind(this)这是什么写法