【Android 音視頻開發(fā)打怪升級:OpenGL渲染視頻畫面篇】三、OpenGL渲染多視頻瘫絮,實現(xiàn)畫中畫

【聲 明】

首先涨冀,這一系列文章均基于自己的理解和實踐,可能有不對的地方麦萤,歡迎大家指正鹿鳖。
其次,這是一個入門系列频鉴,涉及的知識也僅限于夠用栓辜,深入的知識網(wǎng)上也有許許多多的博文供大家學習了。
最后垛孔,寫文章過程中藕甩,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享狭莱。

碼字不易僵娃,轉載請注明出處!

教程代碼:【Github傳送門

目錄

一腋妙、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三唁桩、Android FFmpeg音視頻解碼篇
  • 1,F(xiàn)Fmpeg so庫編譯
  • 2耸棒,Android 引入FFmpeg
  • 3荒澡,Android FFmpeg視頻解碼播放
  • 4,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5榆纽,Android FFmpeg+OpenGL ES播放視頻
  • 6仰猖,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
  • 7捏肢,Android FFmpeg視頻編碼

本文你可以了解到

渲染多視頻畫面奈籽,是實現(xiàn)音視頻編輯的基礎,本文將介紹如何將多個視頻畫面渲染到OpenGL中鸵赫,以及如何對畫面進行混合衣屏、縮放、移動等辩棒。

寫在前面

距離上次更新已經(jīng)有兩個星期狼忱,由于這段時間事情比較多,還請各位關注本系列文章的小伙伴見諒一睁,一有時間我會加緊碼字钻弄,感謝大家的關注和督促。

下面就來看看如何在OpenGL中渲染多視頻畫面者吁。

一窘俺、渲染多畫面

上篇文章中,詳細的講解了如何通過OpenGL渲染視頻畫面复凳,以及對視頻畫面進行比例矯正瘤泪,基于前面系列文章中封裝好的工具灶泵,可以非常容易地實現(xiàn)在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中為每個繪制器設置顯示區(qū)域寬高。
  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中渲染举塔。
并且,你應該發(fā)現(xiàn)了求泰,渲染多個視頻央渣,其實就是生成多個紋理ID,利用這個ID生成一個Surface渲染表面渴频,最后把這個Surface給到解碼器MediaCodec渲染即可芽丹。

由于我這里使用的兩個視頻都是1920*1080的寬高,所以會發(fā)現(xiàn)卜朗,兩個視頻只顯示了一個拔第,因為重疊在一起了。

兩個畫面如下:

第一個畫面
第二個畫面

二场钉、嘗一下視頻編輯的味道

現(xiàn)在蚊俺,兩個視頻疊加在一起,看不到底下的視頻逛万,那么泳猬,我們來改變一下上面這個視頻的alpha值,讓它變成半透明,不就可以看到下面的視頻了嗎暂殖?

1)實現(xiàn)半透

首先价匠,為了統(tǒng)一,在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()
    
    //新增調節(jié)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())
        //將坐標數(shù)據(jù)轉換為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: 創(chuàng)建瞬欧、編譯并啟動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())

            //創(chuàng)建OpenGL ES程序贷屎,注意:需要在OpenGL渲染線程中創(chuàng)建,否則無法渲染
            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)
        //配置邊緣過渡參數(shù)
        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)
        //設置著色器參數(shù)艘虎, 第二個參數(shù)表示一個頂點包含的數(shù)據(jù)數(shù)量唉侄,這里為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 {
        //根據(jù)type創(chuàng)建頂點著色器或者片元著色器
        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,最后給到了片元著色器疏魏。


簡單講一下如何傳遞參數(shù)到片元著色器停做。
要把Java中的值傳遞到片元著色器中,直接傳值是不行的大莫,需要通過頂點著色器蛉腌,間接傳遞。

頂點著色器輸入與輸出
  • 輸入

build-in變量,此類變量為opengl內建參數(shù)烙丛,可以看成是opengl的繪制上下文信息

uniform變量:一般用于Java程序傳入變換矩陣舅巷,材質,光照參數(shù)和顏色等信息河咽。如:uniform mat4 uMatrix;

attribute變量:一般用來傳入一些頂點的數(shù)據(jù)钠右,如:頂點坐標,法線忘蟹,紋理坐標飒房,頂點顏色等。

  • 輸出

build-in變量:即glsl的內建變量媚值,如:gl_Position狠毯。

varying變量:用于頂點著色器向片元著色器傳遞數(shù)據(jù)。需要注意的是:這種變量必須在頂點著色器和片元著色器中褥芒,聲明必須一致嚼松。比如上面的inAlpha。

片元著色器輸入與輸出
  • 輸入

build-in變量:同頂點著色器锰扶。

varying變量:用于作為頂點著色器數(shù)據(jù)的輸入惜颇,與頂點著色器聲明一致

  • 輸出

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)
    }

    //...
}

當你以為可以完美的輸出一個半透明的畫面時乾翔,會發(fā)現(xiàn)畫面依然不是透明的。為啥施戴?

因為沒有開啟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)
    }
}

這樣,就可以看到一個半透明的視頻猜揪,疊加在另一個視頻上面啦惭墓。

半透明畫面

怎么樣,是不是嗅到一股視頻編輯的騷味而姐?

這其實就是最基礎的視頻編輯原理了诅妹,基本上所有的視頻編輯都是基于著色器,去做畫面的變換毅人。

接下來再來看下兩個基本的變換:移動和縮放吭狡。

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方法進行移動變換即可。

但是為了矯正畫面的比例膛虫,上篇文章詳細的介紹過草姻,視頻畫面是經(jīng)過縮放的,因此當前畫面的矩陣并非單位矩陣稍刀。

為此撩独,要平移畫面,就需要對x账月,y综膀,z進行相應的縮放處理(否則移動的距離將被原矩陣中的縮放因子改變)。

那么捶障,有兩種辦法可以使畫面按照正常的距離移動:

  1. 將矩陣還原為單位矩陣->移動->再縮放
  2. 使用當前矩陣->縮放移動距離->移動

很多人都是使用第一種僧须,這里使用第二種。

  • 記錄縮放比例

上一篇文章中项炼,介紹了如何計算縮放系數(shù):

ratio = videoRatio * worldRatio 
或
ratio = videoRatio / worldRatio

分別對應寬或者高的縮放系數(shù)担平。在VideoDrawer中,分別把寬高的縮放系數(shù)記錄下來锭部。

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)
    }
    
    // ......
}

代碼中您市,根據(jù)縮放寬或高觉痛,分別記錄對應的寬高縮放比。

接著茵休,在translate方法中,對dx和dy分別做了縮放。那么縮放是如何得出的呢散怖?

  • 計算移動縮放比

首先晒屎,來看下普通矩陣平移是如何計算縮放的。

普通矩陣平移縮放系數(shù)計算

可以看到钉鸯,一個單位矩陣吧史,在Y方向上放大了2倍以后,經(jīng)過Matrix.translateM變換唠雕,實際平移的距離是原來的2倍扣蜻。

那么為了將移動的距離還原回來,需要把這個倍數(shù)除去及塘。

最終得到:

sx = dx / w_ratio
sy = dy / h_ratio

接下來看看莽使,如何計算OpenGL視頻畫面的移動縮放系數(shù)。

畫面移動縮放系數(shù)計算

第一個是矩陣是OpenGL正交投影矩陣笙僚,我們已經(jīng)知道left和right芳肌,top和bottom互為反數(shù),并且等于視頻畫面的縮放比w_ratio肋层,h_ratio(不清楚的亿笤,請看上一篇文章),因此可以簡化成為右邊的矩陣栋猖。

經(jīng)過Matrix.translateM進行轉換以后净薛,得到的平移分別為:

x方向:1/w_ratio * dx

y方向:1/h_ratio * dy

因此,可以得出正確的平移量為:

sx = dx * w_ratio

sy = dy * h_ratio

但是蒲拉,為何代碼中的平移系數(shù)都乘以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)
GLSurfaceView坐標
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)
}

這樣横媚,就可以隨便移動畫面啦。

移動畫面
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對應的矩陣縮放的位置乘以縮放倍數(shù)穷躁。

在VideoDrawer中添加一個縮放的方法scale:

class VideoDrawer : IDrawer {

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

這里要注意的一點是,設置完縮放系數(shù)的時候因妇,要把該縮放系數(shù)累計到原來的投影矩陣的縮放系數(shù)中问潭,這樣在平移的時候才能正確縮放移動距離猿诸。

注意:這里是 (原來的縮放系數(shù) / 正要縮放的系數(shù)),而非“乘”睦授。因為縮放投影矩陣的縮放比例是“越大两芳,縮的越小”(可以再去看下正交投影的矩陣摔寨,left去枷、right、top是复、bottom是分母)

最后給畫面設置一個縮放系數(shù)删顶,比如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)

    // 設置縮放系數(shù)
    Handler().postDelayed({
        drawer.scale(0.5f, 0.5f)
    }, 1000)
}

效果如下:

縮放移動

三淑廊、后話

以上就是在音視頻開發(fā)中使用到的最基礎的知識逗余,但千萬不要小瞧這些知識,許多酷炫的效果其實都是基于這些最簡單的變換去實現(xiàn)的季惩,希望大家有所收獲录粱。

咱們下篇見!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載画拾,如需轉載請通過簡信或評論聯(lián)系作者啥繁。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市青抛,隨后出現(xiàn)的幾起案子旗闽,更是在濱河造成了極大的恐慌,老刑警劉巖蜜另,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适室,死亡現(xiàn)場離奇詭異,居然都是意外死亡举瑰,警方通過查閱死者的電腦和手機捣辆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來此迅,“玉大人汽畴,你說我怎么就攤上這事∮势ǎ” “怎么了整袁?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佑吝。 經(jīng)常有香客問我坐昙,道長,這世上最難降的妖魔是什么芋忿? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任炸客,我火速辦了婚禮疾棵,結果婚禮上,老公的妹妹穿的比我還像新娘痹仙。我一直安慰自己是尔,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布开仰。 她就那樣靜靜地躺著拟枚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪众弓。 梳的紋絲不亂的頭發(fā)上恩溅,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音谓娃,去河邊找鬼脚乡。 笑死,一個胖子當著我的面吹牛滨达,可吹牛的內容都是我干的奶稠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼捡遍,長吁一口氣:“原來是場噩夢啊……” “哼锌订!你這毒婦竟也來了?” 一聲冷哼從身側響起稽莉,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤瀑志,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后污秆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劈猪,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年良拼,在試婚紗的時候發(fā)現(xiàn)自己被綠了战得。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡庸推,死狀恐怖常侦,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情贬媒,我是刑警寧澤聋亡,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站际乘,受9級特大地震影響坡倔,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一罪塔、第九天 我趴在偏房一處隱蔽的房頂上張望投蝉。 院中可真熱鬧,春花似錦征堪、人聲如沸瘩缆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庸娱。三九已至,卻和暖如春爽锥,著一層夾襖步出監(jiān)牢的瞬間涌韩,已是汗流浹背畔柔。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工氯夷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人靶擦。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓腮考,卻偏偏與公主長得像,于是被迫代替她去往敵國和親玄捕。 傳聞我的和親對象是個殘疾皇子踩蔚,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容