Android OpenGL ES 应用开发OpenGLAndroid OpenGL

Android OpenGL ES 10.1 视频播放器

2019-03-11  本文已影响11人  Benhero

课程介绍

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

一. 视频播放器搭建

1. 视图容器

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

2. 必要框架

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

3. 媒体播放器

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

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

mediaplayer_state_diagram

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

①. 绑定纹理ID

//  1. 创建纹理ID
    val 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 : require
        precision 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 width
        displayHeight = if (isDisplayRotate) width else height
        retriever.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,这代表了视频若想要正确播放,那么需要顺时针方向旋转这个角度值。

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

/**
 * Buffer工具类
 *
 * @author Benhero
 */
object BufferUtil {
    /**
     * Float类型占4Byte
     */
    val BYTES_PER_FLOAT = 4
    /**
     * Short类型占2Byte
     */
    val BYTES_PER_SHORT = 2

    /**
     * 创建一个FloatBuffer
     */
    fun createFloatBuffer(array: FloatArray): FloatBuffer {
        val buffer = ByteBuffer
                // 分配顶点坐标分量个数 * Float占的Byte位数
                .allocateDirect(array.size * BYTES_PER_FLOAT)
                // 按照本地字节序排序
                .order(ByteOrder.nativeOrder())
                // Byte类型转Float类型
                .asFloatBuffer()

        // 将Dalvik的内存数据复制到Native内存中
        buffer.put(array)
        return buffer
    }

    /**
     * 创建一个FloatBuffer
     */
    fun createShortBuffer(array: ShortArray): ShortBuffer {
        val buffer = ByteBuffer
                // 分配顶点坐标分量个数 * Float占的Byte位数
                .allocateDirect(array.size * BYTES_PER_SHORT)
                // 按照本地字节序排序
                .order(ByteOrder.nativeOrder())
                // Byte类型转Float类型
                .asShortBuffer()

        // 将Dalvik的内存数据复制到Native内存中
        buffer.put(array)
        return buffer
    }
}

问题解决后效果如下图:


视频播放器方向问题解决

四. 解决视频比例问题

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

问题现象如下图:


视频比例问题.png

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

/**
 * 自动适配比例的布局
 */
class AspectFrameLayout : FrameLayout {

    private var mTargetAspect = -1.0
    /**
     * 是否自动适配尺寸
     */
    private var mIsAutoFit = true

    constructor(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 = aspectRatio
            requestLayout()
        }
    }

    fun setAutoFit(autoFit: Boolean) {
        mIsAutoFit = autoFit
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var widthMeasure = widthMeasureSpec
        var heightMeasure = heightMeasureSpec
        if (!mIsAutoFit) {
            super.onMeasure(widthMeasure, heightMeasure)
            return
        }

        if (mTargetAspect > 0) {
            var initialWidth = View.MeasureSpec.getSize(widthMeasure)
            var initialHeight = View.MeasureSpec.getSize(heightMeasure)

            val horizontalPadding = paddingLeft + paddingRight
            val verticalPadding = paddingTop + paddingBottom
            initialWidth -= horizontalPadding
            initialHeight -= verticalPadding

            val viewAspectRatio = initialWidth.toDouble() / initialHeight
            val aspectDiff = mTargetAspect / viewAspectRatio - 1

            if (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 += horizontalPadding
                initialHeight += verticalPadding
                widthMeasure = 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,就可以解决比例问题。

解决效果如下图


视频比例问题解决

其他

GitHub工程

本系列课程所有相关代码请参考我的GitHub项目⭐GLStudio⭐,喜欢的请给个小星星。😃

上一篇下一篇

猜你喜欢

热点阅读