欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

程序员文章站 2022-07-14 20:38:27
...

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

【声 明】

首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

码字不易,转载请注明出处!

教程代码:【Github传送门

目录

一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
三、Android FFmpeg音视频解码篇
  • 1,FFmpeg so库编译
  • 2,Android 引入FFmpeg
  • 3,Android FFmpeg视频解码播放
  • 4,Android FFmpeg+OpenSL ES音频解码播放
  • 5,Android FFmpeg+OpenGL ES播放视频
  • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
  • 7,Android FFmpeg视频编码

本文你可以了解到

渲染多视频画面,是实现音视频编辑的基础,本文将介绍如何将多个视频画面渲染到OpenGL中,以及如何对画面进行混合、缩放、移动等。

写在前面

距离上次更新已经有两个星期,由于这段时间事情比较多,还请各位关注本系列文章的小伙伴见谅,一有时间我会加紧码字,感谢大家的关注和督促。

下面就来看看如何在OpenGL中渲染多视频画面。

一、渲染多画面

上篇文章中,详细的讲解了如何通过OpenGL渲染视频画面,以及对视频画面进行比例矫正,基于前面系列文章中封装好的工具,可以非常容易地实现在OpenGL中渲染多个视频画面。

上文的OpenGL Render非常简单如下:

class SimpleRender(private val mDrawer: IDrawer): GLSurfaceView.Renderer {

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0f, 0f, 0f, 0f)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        mDrawer.setTextureID(OpenGLTools.createTextureIds(1)[0])
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        mDrawer.setWorldSize(width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        mDrawer.draw()
    }
}

只支持一个Drawer,这里改造一下,把Drawer修改为列表,以支持多个绘制器。

class SimpleRender: GLSurfaceView.Renderer {

    private val drawers = mutableListOf<IDrawer>()

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0f, 0f, 0f, 0f)
        val textureIds = OpenGLTools.createTextureIds(drawers.size)
        for ((idx, drawer) in drawers.withIndex()) {
            drawer.setTextureID(textureIds[idx])
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        for (drawer in drawers) {
            drawer.setWorldSize(width, height)
        }
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
        drawers.forEach {
            it.draw()
        }
    }

    fun addDrawer(drawer: IDrawer) {
        drawers.add(drawer)
    }
}

同样非常简单,

  1. 增加一个addDrawer方法,用来添加多个绘制器。
  2. 在onSurfaceCreated中为每个绘制器设置一个纹理ID。
  3. 在onSurfaceChanged中为每个绘制器设置显示区域宽高。
  4. 在onDrawFrame中,遍历所有绘制器,启动绘制。

接着,新建一个新页面,生成多个解码器和绘制器。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto">
    <android.opengl.GLSurfaceView
            android:id="@+id/gl_surface"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
class MultiOpenGLPlayerActivity: AppCompatActivity() {
    private val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
    private val path2 = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_2.mp4"

    private val render = SimpleRender()

    private val threadPool = Executors.newFixedThreadPool(10)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_opengl_player)
        initFirstVideo()
        initSecondVideo()
        initRender()
    }
    
    private fun initFirstVideo() {
        val drawer = VideoDrawer()
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initPlayer(path, Surface(it), true)
        }
        render.addDrawer(drawer)
    }

    private fun initSecondVideo() {
        val drawer = VideoDrawer()
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initPlayer(path2, Surface(it), false)
        }
        render.addDrawer(drawer)
    }

    private fun initPlayer(path: String, sf: Surface, withSound: Boolean) {
        val videoDecoder = VideoDecoder(path, null, sf)
        threadPool.execute(videoDecoder)
        videoDecoder.goOn()

        if (withSound) {
            val audioDecoder = AudioDecoder(path)
            threadPool.execute(audioDecoder)
            audioDecoder.goOn()
        }
    }
    
    private fun initRender() {
        gl_surface.setEGLContextClientVersion(2)
        gl_surface.setRenderer(render)
    }
}

代码比较简单,通过之前封装好的解码工具和绘制工具,添加了两个视频画面的渲染。

当然了,你可以添加更多的画面到OpenGL中渲染。
并且,你应该发现了,渲染多个视频,其实就是生成多个纹理ID,利用这个ID生成一个Surface渲染表面,最后把这个Surface给到解码器MediaCodec渲染即可。

由于我这里使用的两个视频都是1920*1080的宽高,所以会发现,两个视频只显示了一个,因为重叠在一起了。

两个画面如下:

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

二、尝一下视频编辑的味道

现在,两个视频叠加在一起,看不到底下的视频,那么,我们来改变一下上面这个视频的alpha值,让它变成半透明,不就可以看到下面的视频了吗?

1)实现半透

首先,为了统一,在IDrawer中新加一个接口:

interface IDrawer {
    fun setVideoSize(videoW: Int, videoH: Int)
    fun setWorldSize(worldW: Int, worldH: Int)
    fun draw()
    fun setTextureID(id: Int)
    fun getSurfaceTexture(cb: (st: SurfaceTexture)->Unit) {}
    fun release()
    
    //新增调节alpha接口
    fun setAlpha(alpha: Float)
}

在VideoDrawer中,保存该值。

为了方便查看,这里将整个VideoDrawer都贴出来(不想看的可跳过看下面增加的部分):

class VideoDrawer : IDrawer {

    // 顶点坐标
    private val mVertexCoors = floatArrayOf(
        -1f, -1f,
        1f, -1f,
        -1f, 1f,
        1f, 1f
    )

    // 纹理坐标
    private val mTextureCoors = floatArrayOf(
        0f, 1f,
        1f, 1f,
        0f, 0f,
        1f, 0f
    )

    private var mWorldWidth: Int = -1
    private var mWorldHeight: Int = -1
    private var mVideoWidth: Int = -1
    private var mVideoHeight: Int = -1

    private var mTextureId: Int = -1

    private var mSurfaceTexture: SurfaceTexture? = null

    private var mSftCb: ((SurfaceTexture) -> Unit)? = null

    //OpenGL程序ID
    private var mProgram: Int = -1

    //矩阵变换接收者
    private var mVertexMatrixHandler: Int = -1
    // 顶点坐标接收者
    private var mVertexPosHandler: Int = -1
    // 纹理坐标接收者
    private var mTexturePosHandler: Int = -1
    // 纹理接收者
    private var mTextureHandler: Int = -1
    // 半透值接收者
    private var mAlphaHandler: Int = -1

    private lateinit var mVertexBuffer: FloatBuffer
    private lateinit var mTextureBuffer: FloatBuffer

    private var mMatrix: FloatArray? = null

    private var mAlpha = 1f

    init {
        //【步骤1: 初始化顶点坐标】
        initPos()
    }

    private fun initPos() {
        val bb = ByteBuffer.allocateDirect(mVertexCoors.size * 4)
        bb.order(ByteOrder.nativeOrder())
        //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
        mVertexBuffer = bb.asFloatBuffer()
        mVertexBuffer.put(mVertexCoors)
        mVertexBuffer.position(0)

        val cc = ByteBuffer.allocateDirect(mTextureCoors.size * 4)
        cc.order(ByteOrder.nativeOrder())
        mTextureBuffer = cc.asFloatBuffer()
        mTextureBuffer.put(mTextureCoors)
        mTextureBuffer.position(0)
    }

    private fun initDefMatrix() {
        if (mMatrix != null) return
        if (mVideoWidth != -1 && mVideoHeight != -1 &&
            mWorldWidth != -1 && mWorldHeight != -1) {
            mMatrix = FloatArray(16)
            var prjMatrix = FloatArray(16)
            val originRatio = mVideoWidth / mVideoHeight.toFloat()
            val worldRatio = mWorldWidth / mWorldHeight.toFloat()
            if (mWorldWidth > mWorldHeight) {
                if (originRatio > worldRatio) {
                    val actualRatio = originRatio / worldRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -actualRatio, actualRatio,
                        -1f, 1f,
                        3f, 5f
                    )
                } else {// 原始比例小于窗口比例,缩放宽度会导致宽度度超出,因此,宽度以窗口为准,缩放高度
                    val actualRatio = worldRatio / originRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -1f, 1f,
                        -actualRatio, actualRatio,
                        3f, 5f
                    )
                }
            } else {
                if (originRatio > worldRatio) {
                    val actualRatio = originRatio / worldRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -1f, 1f,
                        -actualRatio, actualRatio,
                        3f, 5f
                    )
                } else {// 原始比例小于窗口比例,缩放高度会导致高度超出,因此,高度以窗口为准,缩放宽度
                    val actualRatio = worldRatio / originRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -actualRatio, actualRatio,
                        -1f, 1f,
                        3f, 5f
                    )
                }
            }

            //设置相机位置
            val viewMatrix = FloatArray(16)
            Matrix.setLookAtM(
                viewMatrix, 0,
                0f, 0f, 5.0f,
                0f, 0f, 0f,
                0f, 1.0f, 0f
            )
            //计算变换矩阵
            Matrix.multiplyMM(mMatrix, 0, prjMatrix, 0, viewMatrix, 0)
        }
    }

    override fun setVideoSize(videoW: Int, videoH: Int) {
        mVideoWidth = videoW
        mVideoHeight = videoH
    }

    override fun setWorldSize(worldW: Int, worldH: Int) {
        mWorldWidth = worldW
        mWorldHeight = worldH
    }

    override fun setAlpha(alpha: Float) {
        mAlpha = alpha
    }

    override fun setTextureID(id: Int) {
        mTextureId = id
        mSurfaceTexture = SurfaceTexture(id)
        mSftCb?.invoke(mSurfaceTexture!!)
    }

    override fun getSurfaceTexture(cb: (st: SurfaceTexture) -> Unit) {
        mSftCb = cb
    }

    override fun draw() {
        if (mTextureId != -1) {
            initDefMatrix()
            //【步骤2: 创建、编译并启动OpenGL着色器】
            createGLPrg()
            //【步骤3: **并绑定纹理单元】
            activateTexture()
            //【步骤4: 绑定图片到纹理单元】
            updateTexture()
            //【步骤5: 开始渲染绘制】
            doDraw()
        }
    }

    private fun createGLPrg() {
        if (mProgram == -1) {
            val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
            val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())

            //创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
            mProgram = GLES20.glCreateProgram()
            //将顶点着色器加入到程序
            GLES20.glAttachShader(mProgram, vertexShader)
            //将片元着色器加入到程序中
            GLES20.glAttachShader(mProgram, fragmentShader)
            //连接到着色器程序
            GLES20.glLinkProgram(mProgram)

            mVertexMatrixHandler = GLES20.glGetUniformLocation(mProgram, "uMatrix")
            mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
            mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture")
            mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
            mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha")
        }
        //使用OpenGL程序
        GLES20.glUseProgram(mProgram)
    }

    private fun activateTexture() {
        //**指定纹理单元
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        //绑定纹理ID到纹理单元
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId)
        //将**的纹理单元传递到着色器里面
        GLES20.glUniform1i(mTextureHandler, 0)
        //配置边缘过渡参数
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
    }

    private fun updateTexture() {
        mSurfaceTexture?.updateTexImage()
    }

    private fun doDraw() {
        //启用顶点的句柄
        GLES20.glEnableVertexAttribArray(mVertexPosHandler)
        GLES20.glEnableVertexAttribArray(mTexturePosHandler)
        GLES20.glUniformMatrix4fv(mVertexMatrixHandler, 1, false, mMatrix, 0)
        //设置着色器参数, 第二个参数表示一个顶点包含的数据数量,这里为xy,所以为2
        GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
        GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
        GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha)
        //开始绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    }

    override fun release() {
        GLES20.glDisableVertexAttribArray(mVertexPosHandler)
        GLES20.glDisableVertexAttribArray(mTexturePosHandler)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        GLES20.glDeleteTextures(1, intArrayOf(mTextureId), 0)
        GLES20.glDeleteProgram(mProgram)
    }

    private fun getVertexShader(): String {
        return "attribute vec4 aPosition;" +
                "precision mediump float;" +
                "uniform mat4 uMatrix;" +
                "attribute vec2 aCoordinate;" +
                "varying vec2 vCoordinate;" +
                "attribute float alpha;" +
                "varying float inAlpha;" +
                "void main() {" +
                "    gl_Position = uMatrix*aPosition;" +
                "    vCoordinate = aCoordinate;" +
                "    inAlpha = alpha;" +
                "}"
    }

    private fun getFragmentShader(): String {
        //一定要加换行"\n",否则会和下一行的precision混在一起,导致编译出错
        return "#extension GL_OES_EGL_image_external : require\n" +
                "precision mediump float;" +
                "varying vec2 vCoordinate;" +
                "varying float inAlpha;" +
                "uniform samplerExternalOES uTexture;" +
                "void main() {" +
                "  vec4 color = texture2D(uTexture, vCoordinate);" +
                "  gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
                "}"
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        //根据type创建顶点着色器或者片元着色器
        val shader = GLES20.glCreateShader(type)
        //将资源加入到着色器中,并编译
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)

        return shader
    }
}

实际上,相比较之前的绘制器,改变的地方很少:


class VideoDrawer : IDrawer {
    // 省略无关代码......
    
    // 半透值接收者
    private var mAlphaHandler: Int = -1
    
    // 半透明值
    private var mAlpha = 1f
    
    override fun setAlpha(alpha: Float) {
        mAlpha = alpha
    }
    
    private fun createGLPrg() {
        if (mProgram == -1) {
        
            // 省略无关代码......
            
            mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha")
            
            //......
        }
        //使用OpenGL程序
        GLES20.glUseProgram(mProgram)
    }
    
    private fun doDraw() {
    
        // 省略无关代码......
    
        GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha)
        
        //......
    }
    
    private fun getVertexShader(): String {
        return "attribute vec4 aPosition;" +
                "precision mediump float;" +
                "uniform mat4 uMatrix;" +
                "attribute vec2 aCoordinate;" +
                "varying vec2 vCoordinate;" +
                "attribute float alpha;" +
                "varying float inAlpha;" +
                "void main() {" +
                "    gl_Position = uMatrix*aPosition;" +
                "    vCoordinate = aCoordinate;" +
                "    inAlpha = alpha;" +
                "}"
    }

    private fun getFragmentShader(): String {
        //一定要加换行"\n",否则会和下一行的precision混在一起,导致编译出错
        return "#extension GL_OES_EGL_image_external : require\n" +
                "precision mediump float;" +
                "varying vec2 vCoordinate;" +
                "varying float inAlpha;" +
                "uniform samplerExternalOES uTexture;" +
                "void main() {" +
                "  vec4 color = texture2D(uTexture, vCoordinate);" +
                "  gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
                "}"
    }
}

重点关注两个着色器的代码:

在顶点着色器中,传入了一个alpha变量,该值由java代码传入,然后顶点着色器将该值赋值给了inAlpha,最后给到了片元着色器。


简单讲一下如何传递参数到片元着色器。
要把Java中的值传递到片元着色器中,直接传值是不行的,需要通过顶点着色器,间接传递。

顶点着色器输入与输出
  • 输入

build-in变量,此类变量为opengl内建参数,可以看成是opengl的绘制上下文信息

uniform变量:一般用于Java程序传入变换矩阵,材质,光照参数和颜色等信息。如:uniform mat4 uMatrix;

attribute变量:一般用来传入一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。

  • 输出

build-in变量:即glsl的内建变量,如:gl_Position。

varying变量:用于顶点着色器向片元着色器传递数据。需要注意的是:这种变量必须在顶点着色器和片元着色器中,声明必须一致。比如上面的inAlpha。

片元着色器输入与输出
  • 输入

build-in变量:同顶点着色器。

varying变量:用于作为顶点着色器数据的输入,与顶点着色器声明一致

  • 输出

build-in变量:即glsl的内建变量,如:gl_FragColor。


知道了如何传值,其他的就一目了然了。

  1. 获取顶点着色器的alpha,然后在绘制前把值传递进入。
  2. 在片元着色器中,修改从纹理中取出的颜色值的alpha。最后赋值给gl_FragColor进行输出。

接着,在MultiOpenGLPlayerAcitivity中,改变上层画面的半透值


class MultiOpenGLPlayerActivity: AppCompatActivity() {

    // 省略无关代码...
    
    private fun initSecondVideo() {
        val drawer = VideoDrawer()
        // 设置半透值
        drawer.setAlpha(0.5f)
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initPlayer(path2, Surface(it), false)
        }
        render.addDrawer(drawer)
    }

    //...
}

当你以为可以完美的输出一个半透明的画面时,会发现画面依然不是透明的。为啥?

因为没有开启OpenGL混合模式,回到SimpleRender中。

  1. 在onSurfaceCreated中开启混合模式;
  2. 在onDrawFrame中开始绘制每一帧之前,清除屏幕,否则会有画面残留。
class SimpleRender: GLSurfaceView.Renderer {

    private val drawers = mutableListOf<IDrawer>()

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0f, 0f, 0f, 0f)
        
        //------开启混合,即半透明---------
        // 开启很混合模式
        GLES20.glEnable(GLES20.GL_BLEND)
        // 配置混合算法
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
        //------------------------------

        val textureIds = OpenGLTools.createTextureIds(drawers.size)
        for ((idx, drawer) in drawers.withIndex()) {
            drawer.setTextureID(textureIds[idx])
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        for (drawer in drawers) {
            drawer.setWorldSize(width, height)
        }
    }

    override fun onDrawFrame(gl: GL10?) {
        // 清屏,否则会有画面残留
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
        drawers.forEach {
            it.draw()
        }
    }

    fun addDrawer(drawer: IDrawer) {
        drawers.add(drawer)
    }
}

这样,就可以看到一个半透明的视频,叠加在另一个视频上面啦。

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

怎么样,是不是嗅到一股视频编辑的骚味?

这其实就是最基础的视频编辑原理了,基本上所有的视频编辑都是基于着色器,去做画面的变换。

接下来再来看下两个基本的变换:移动和缩放。

2) 移动

接下来,来看看如何通过触摸拖动,来改变视频的位置。

前面文章讲过,图片或视频的移位和缩放,基本都是通过矩阵变换完成的。

Android在Matrix中提供了一个方法用于矩阵的平移:

/**
 * Translates matrix m by x, y, and z in place.
 *
 * @param m matrix
 * @param mOffset index into m where the matrix starts
 * @param x translation factor x
 * @param y translation factor y
 * @param z translation factor z
 */
public static void translateM(
        float[] m, int mOffset,
        float x, float y, float z) {
    for (int i=0 ; i<4 ; i++) {
        int mi = mOffset + i;
        m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
    }
}

其实就是改变了4x4矩阵的最后一行的值。

其中,x,y,z分别是相对于当前位置移动的距离。

这里需要注意的是:平移的变化值,被乘上了缩放的比例。具体大家可以用笔在纸上算一下就知道了。

如果原始矩阵是单位矩阵,直接使用以上translateM方法进行移动变换即可。

但是为了矫正画面的比例,上篇文章详细的介绍过,视频画面是经过缩放的,因此当前画面的矩阵并非单位矩阵。

为此,要平移画面,就需要对x,y,z进行相应的缩放处理(否则移动的距离将被原矩阵中的缩放因子改变)。

那么,有两种办法可以使画面按照正常的距离移动:

  1. 将矩阵还原为单位矩阵->移动->再缩放
  2. 使用当前矩阵->缩放移动距离->移动

很多人都是使用第一种,这里使用第二种。

  • 记录缩放比例

上一篇文章中,介绍了如何计算缩放系数:

ratio = videoRatio * worldRatio 
或
ratio = videoRatio / worldRatio

分别对应宽或者高的缩放系数。在VideoDrawer中,分别把宽高的缩放系数记录下来。

class VideoDrawer : IDrawer {
    
    // 省略无关代码......
    
    private var mWidthRatio = 1f
    private var mHeightRatio = 1f
    
    private fun initDefMatrix() {
        if (mMatrix != null) return
        if (mVideoWidth != -1 && mVideoHeight != -1 &&
            mWorldWidth != -1 && mWorldHeight != -1) {
            mMatrix = FloatArray(16)
            var prjMatrix = FloatArray(16)
            val originRatio = mVideoWidth / mVideoHeight.toFloat()
            val worldRatio = mWorldWidth / mWorldHeight.toFloat()
            if (mWorldWidth > mWorldHeight) {
                if (originRatio > worldRatio) {
                    mHeightRatio = originRatio / worldRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -mWidthRatio, mWidthRatio,
                        -mHeightRatio, mHeightRatio,
                        3f, 5f
                    )
                } else {// 原始比例小于窗口比例,缩放高度度会导致高度超出,因此,高度以窗口为准,缩放宽度
                    mWidthRatio = worldRatio / originRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -mWidthRatio, mWidthRatio,
                        -mHeightRatio, mHeightRatio,
                        3f, 5f
                    )
                }
            } else {
                if (originRatio > worldRatio) {
                    mHeightRatio = originRatio / worldRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -mWidthRatio, mWidthRatio,
                        -mHeightRatio, mHeightRatio,
                        3f, 5f
                    )
                } else {// 原始比例小于窗口比例,缩放高度会导致高度超出,因此,高度以窗口为准,缩放宽度
                    mWidthRatio = worldRatio / originRatio
                    Matrix.orthoM(
                        prjMatrix, 0,
                        -mWidthRatio, mWidthRatio,
                        -mHeightRatio, mHeightRatio,
                        3f, 5f
                    )
                }
            }

            //设置相机位置
            val viewMatrix = FloatArray(16)
            Matrix.setLookAtM(
                viewMatrix, 0,
                0f, 0f, 5.0f,
                0f, 0f, 0f,
                0f, 1.0f, 0f
            )
            //计算变换矩阵
            Matrix.multiplyMM(mMatrix, 0, prjMatrix, 0, viewMatrix, 0)
        }
    }
    
    // 平移
    fun translate(dx: Float, dy: Float) {
        Matrix.translateM(mMatrix, 0, dx*mWidthRatio*2, -dy*mHeightRatio*2, 0f)
    }
    
    // ......
}

代码中,根据缩放宽或高,分别记录对应的宽高缩放比。

接着,在translate方法中,对dx和dy分别做了缩放。那么缩放是如何得出的呢?

  • 计算移动缩放比

首先,来看下普通矩阵平移是如何计算缩放的。

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

可以看到,一个单位矩阵,在Y方向上放大了2倍以后,经过Matrix.translateM变换,实际平移的距离是原来的2倍。

那么为了将移动的距离还原回来,需要把这个倍数除去。

最终得到:

sx = dx / w_ratio
sy = dy / h_ratio

接下来看看,如何计算OpenGL视频画面的移动缩放系数。

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

第一个是矩阵是OpenGL正交投影矩阵,我们已经知道left和right,top和bottom互为反数,并且等于视频画面的缩放比w_ratio,h_ratio(不清楚的,请看上一篇文章),因此可以简化成为右边的矩阵。

经过Matrix.translateM进行转换以后,得到的平移分别为:

x方向:1/w_ratio * dx

y方向:1/h_ratio * dy

因此,可以得出正确的平移量为:

sx = dx * w_ratio

sy = dy * h_ratio

但是,为何代码中的平移系数都乘以2呢?即

fun translate(dx: Float, dy: Float) {
    Matrix.translateM(mMatrix, 0, dx*mWidthRatio*2, -dy*mHeightRatio*2, 0f)
}

首先理解一下,这里的dx和dy指的是什么呢?

dx = (curX - prevX) / GLSurfaceView_Width

dy = (curY - prevY) / GLSurfaceView_Height

其中,
curX/curY:为当前手指触摸点的x/y坐标
pervX/prevY:为上一个手指触摸点的x/y坐标

即dx,dy是归一化的距离,范围(0~1)。

对应了OpenGL的世界坐标:

x方向为 (left, right) -> (-w_ratio, w_ratio)

y方向为 (top, bottom) ->(-h_ratio, h_ratio)

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

实际上整个OpenGL的世界坐标宽为:2倍的w_ratio;高为2倍的h_ratio。所以要把实际(0~1)换算为对应的世界坐标中的距离,需要乘以2,才能得到正确的移动距离。

最后,还有一点要注意的是,y方向的平移前面加了一个负号,这是因为Android屏幕Y轴的正方向是向下,而OpenGL世界坐标Y轴方向是向上的,正好相反。

  • 获取触摸距离,并平移画面

为了获取手指的触摸点,需要自定义一个GLSurfaceView。

class DefGLSurfaceView : GLSurfaceView {

    constructor(context: Context): super(context)

    constructor(context: Context, attrs: AttributeSet): super(context, attrs)

    private var mPrePoint = PointF()

    private var mDrawer: VideoDrawer? = null

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mPrePoint.x = event.x
                mPrePoint.y = event.y
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = (event.x - mPrePoint.x) / width
                val dy = (event.y - mPrePoint.y) / height
                mDrawer?.translate(dx, dy)
                mPrePoint.x = event.x
                mPrePoint.y = event.y
            }
        }
        return true
    }

    fun addDrawer(drawer: VideoDrawer) {
        mDrawer = drawer
    }
}

代码很简单,为了方便演示,只添加了一个绘制器,也没有去判断手指是否触摸到实际画面的位置,只要有触摸移动,就平移画面。

然后把它放到页面中使用

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <com.cxp.learningvideo.opengl.DefGLSurfaceView
            android:id="@+id/gl_surface"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>

最后,在Activity中调用addDrawer,把上面那个画面的绘制器设置给DefGLSurfaceView。

private fun initSecondVideo() {
    val drawer = VideoDrawer()
    drawer.setVideoSize(1920, 1080)
    drawer.getSurfaceTexture {
        initPlayer(path2, Surface(it), false)
    }
    render.addDrawer(drawer)
    
    //设置绘制器,用于触摸移动
    gl_surface.addDrawer(drawer)
}

这样,就可以随便移动画面啦。

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

3)缩放

相对于移动缩放显得要简单的多。

Android的Matrix提供一个矩阵缩放方法:


/**
 * Scales matrix m in place by sx, sy, and sz.
 *
 * @param m matrix to scale
 * @param mOffset index into m where the matrix starts
 * @param x scale factor x
 * @param y scale factor y
 * @param z scale factor z
 */
public static void scaleM(float[] m, int mOffset,
        float x, float y, float z) {
    for (int i=0 ; i<4 ; i++) {
        int mi = mOffset + i;
        m[     mi] *= x;
        m[ 4 + mi] *= y;
        m[ 8 + mi] *= z;
    }
}

这个方法也非常简单,就是将x,y,z对应的矩阵缩放的位置乘以缩放倍数。

在VideoDrawer中添加一个缩放的方法scale:

class VideoDrawer : IDrawer {

    // 省略无关代码.......
    
    fun scale(sx: Float, sy: Float) {
        Matrix.scaleM(mMatrix, 0, sx, sy, 1f)
        mWidthRatio /= sx
        mHeightRatio /= sy
    }
    
    // ......
}

这里要注意的一点是,设置完缩放系数的时候,要把该缩放系数累计到原来的投影矩阵的缩放系数中,这样在平移的时候才能正确缩放移动距离。

注意:这里是 (原来的缩放系数 / 正要缩放的系数),而非“乘”。因为缩放投影矩阵的缩放比例是“越大,缩的越小”(可以再去看下正交投影的矩阵,left、right、top、bottom是分母)

最后给画面设置一个缩放系数,比如0.5f。

private fun initSecondVideo() {
    val drawer = VideoDrawer()
    drawer.setAlpha(0.5f)
    drawer.setVideoSize(1920, 1080)
    drawer.getSurfaceTexture {
        initPlayer(path2, Surface(it), false)
    }
    render.addDrawer(drawer)
    gl_surface.addDrawer(drawer)

    // 设置缩放系数
    Handler().postDelayed({
        drawer.scale(0.5f, 0.5f)
    }, 1000)
}

效果如下:

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

三、后话

以上就是在音视频开发中使用到的最基础的知识,但千万不要小瞧这些知识,许多酷炫的效果其实都是基于这些最简单的变换去实现的,希望大家有所收获。

咱们下篇见!

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】三、OpenGL渲染多视频,实现画中画

相关标签: 音视频开发