Android視頻融合特效播放與渲染

AlphaPlayerPlus

一個有趣且很有創(chuàng)意的視頻特效項目。

https://github.com/duqian291902259/AlphaPlayerPlus

前言

直播產(chǎn)品,需要更多炫酷的禮物特效,比如飛機特效,跑車特效,生日蛋糕融特效等,融合了直播流畫面的特效。所以在字節(jié)開源的alphaPlayer庫和特效VAP庫的基礎(chǔ)上進行改造,實現(xiàn)融合特效渲染。
支持將用戶的頭像、昵稱、當(dāng)前直播間的直播視頻流等元素動態(tài)融合進視頻特效中,增強用戶的交互感和體驗感。

Android實現(xiàn)

已經(jīng)將原有的alphaPlayer進行升級改造,支持將用戶的頭像、昵稱、當(dāng)前直播間的直播視頻流等元素動態(tài)融合進視頻特效中,增強用戶的交互感和體驗感。

融合特效的技術(shù)原理

自研特效sdk,負責(zé)渲染視頻特效。
美術(shù)負責(zé)制作特效視頻,制作時將特效的顏色信息通過等分的視頻區(qū)域存儲,視頻的左半邊存放了特效原始的RGB信息,視頻的右半邊存放了特效的透明度和遮罩的透明度等信息。

透明視頻的渲染

特效渲染時,讀取左半部分紋理的RGB值,作為原始視頻的主體顏色,右半部分紋理的R通道值作為Alpha值,得到視頻紋理的rgba值,進行著色渲染,得到帶透明度的特效紋理,如下圖所示:


基于RGBA通道的透明視頻融合特效渲染.png

視頻融合遮罩素材

通常特效中的融合區(qū)域,會隨著場景的變換,產(chǎn)生近大遠小的透視3D效果,而不是維持標(biāo)準的矩形形狀。在圖形渲染技術(shù)中,對于渲染的頂點坐標(biāo)組成非平行四邊形形狀,GPU默認對組成四邊形的兩個三角形分別采用線性紋理插值的方式,渲染的結(jié)果可以看到兩個三角形雖然在接縫處是連續(xù)的,單其導(dǎo)數(shù)(切線和雙線向量)在此處不連續(xù),效果無法達到預(yù)期,如圖2所示。為解決這一問題,需要使用投影插值的方式,將融合紋理渲染的2D頂點坐標(biāo),通過透視變換的到貼合特效的形狀,從而模擬3D的視覺效果??梢詫⑦@一過程抽象成將一個矩形,經(jīng)過透視變換,得到一個非平行四邊形的過程。

將帶透明度的特效紋理,與融合元素的紋理,根據(jù)資源右側(cè)部分的G值進行融合渲染。融合渲染的RGB= (特效紋理RGB)(1-右側(cè)G)+(融合紋理RGB)(右側(cè)G),融合渲染的Alpha值=(右側(cè)R)(1-右側(cè)G)+(右側(cè)G)(右側(cè)G)。得到融合后的特效紋理,最終渲染到設(shè)備屏幕上。

融合特效的數(shù)據(jù)管理

原始視頻與遮罩素材的融合過程以及而遮罩的渲染,是在原有右邊灰色視頻的基礎(chǔ)上,采用G通道值表示遮罩的區(qū)域,后續(xù)遮罩渲染需要根據(jù)G通道的值作為融合的透明度。
因視頻數(shù)據(jù)幀比較多,里面的遮罩位置、個數(shù)、文字內(nèi)容、文字顏色、圖片展示形式,千差萬別,渲染時直接處理,有很多東西是程序未知的,所以需要對素材預(yù)處理,提前得到這些信息,生成一份渲染專用的json配置文件。這份文件要么放在視頻中,要么服務(wù)器下發(fā)(目前我們這兩種方式都會采用,遮罩相關(guān)的信息放視頻中,與用戶相關(guān)的信息接口提供)。

1,預(yù)處理工具

負責(zé)獲取特效渲染所需的資源數(shù)據(jù)和遮罩數(shù)據(jù)。遮罩素材,需要專門導(dǎo)出一個視頻(右側(cè)部分)用于識別遮罩的區(qū)域和色值。

2,Mp4播放渲染。

其中對遮罩的識別、坐標(biāo)位置的確定、變換矩陣的生成,由工具識別素材視頻,最終導(dǎo)出json格式,寫入進mp4容器的自定義box中。
我們端上需要解析出json,然后封裝成對應(yīng)的實體類,方便渲染時讀取遮罩信息,并替換成實際的文字、圖片、直播幀數(shù)據(jù)。

融合特效渲染步驟

1,原有的特效視頻渲染,支持帶alpha通道的mp4視頻。我們可以得到視頻紋理id:mVideoTextureId,包含左右灰色和透明區(qū)域。
2,使用FBO,對文字,圖片,直播流的數(shù)據(jù)進行二次處理,確定渲染的頂點位置和紋理坐標(biāo),渲染成屏幕大小的紋理:fboTextureId
3,將mVideoTextureId和fboTextureId傳入最終融合的著色器處理,得到最終的展示效果。
以上三個步驟,面涉及到很多細節(jié),需要了解OpenGL的坐標(biāo)體系,繪制步驟和基本原理。具體可以參看項目代碼。

流程圖

融合渲染流程圖.png

實現(xiàn)的效果

畫中畫效果.png

圖片和文字渲染

接入說明

可以參看demo項目,暫未開放。demo只提供圖片和文字融合效果的實現(xiàn)。
1,可以實現(xiàn)圖片、視頻流融合效果
2,圖片+文字的融合效果
3,配置入口:PlayerController.get(configuration, mediaPlayer)
4,VideoGiftView,只是封裝了PlayerController和播放容器。
5,音視頻播放器可以自定義,也可以用現(xiàn)有的實現(xiàn)。AlphaMediaPlayer便于控制解碼、渲染次數(shù)。
6,其他的播放器( DefaultSystemPlayer、ExoPlayerImpl)播放的話,渲染遮罩的幀索引需要將json中的index-1,否則無法對齊原始素材效果
7,ALog:內(nèi)部log輸出接口,外部可以自行實現(xiàn)打印。
8,IFetchResource:資源接口:外部傳入文本、圖片等資源給遮罩幀使用。

Android API說明

初始化配置信息、PlayerController

val configuration = Configuration(context,owner)
        //  GLTextureView supports custom display layer, but GLSurfaceView has better performance, and the GLSurfaceView is default.
        configuration.alphaVideoViewType = AlphaVideoViewType.GL_SURFACE_VIEW
        // AlphaMediaPlayer便于控制解碼、渲染次數(shù),其他的播放器,渲染幀索引需要將json中的數(shù)字-1,否則無法對齊
        //val mediaPlayer = DefaultSystemPlayer()
        //val mediaPlayer = ExoPlayerImpl(context)
        val mediaPlayer = AlphaMediaPlayer(context)
        mPlayerController = PlayerController.get(configuration, mediaPlayer)
        .//設(shè)置監(jiān)聽
        mPlayerController?.let {
            it.setPlayerAction(playerAction)
            it.setFetchResource(fetchResource)
            //it.setMonitor(monitor)
        }

設(shè)置流數(shù)據(jù)監(jiān)聽

    mPlayerController.setOnRenderLiveFrameListener(() -> {
        //通知外部,有遮罩需要用到視頻流畫面,開始渲染直播畫面,自定實現(xiàn)監(jiān)聽流,本demo無流數(shù)據(jù)來源,暫不提供實現(xiàn)。
        // 然后將流里面的直播畫面的數(shù)據(jù)共享到surfaceTexture,通過以下調(diào)用傳回給sdk內(nèi)部渲染:
    });
 

IPlayerAction接口

private val playerAction = object : IPlayerAction {
        override fun onVideoSizeChanged(videoWidth: Int, videoHeight: Int, scaleType: ScaleType) {
            Log.i(TAG, "視頻實際的寬高改變,用戶改變渲染容器size:onVideoSizeChanged,videoWidth=$videoWidth, videoHeight=$videoHeight,scaleType=$scaleType")
        }
        override fun startAction() {
            Log.i(TAG, "開始播放")
        }
        override fun endAction(status: Int, mVideoPath: String?) {
            Log.i(TAG, "結(jié)束播放,錯誤碼和出錯的url,方便做重新播放邏輯")
        }
    }

資源接口說明

interface IFetchResource {
    // 獲取圖片 (暫時不支持Bitmap.Config.ALPHA_8 主要是因為一些機型opengl兼容問題)
    fun fetchImage(resource: Resource, result: (Bitmap?) -> Unit)
    // 獲取文字
    fun fetchText(resource: Resource, result: (String?) -> Unit)
    // 資源釋放通知
    fun releaseResource(resources: List<Resource>)
}

自定義Log

回調(diào)中使用自己的log實現(xiàn)

private fun initLog() {
        ALog.isDebug = BuildConfig.DEBUG
        ALog.log = ALogImp()

class ALogImp : IALog {
        override fun i(tag: String, msg: String) {
            Log.i(tag, msg)
        }
        override fun d(tag: String, msg: String) {
            Log.d(tag, msg)
        }
        override fun e(tag: String, msg: String) {
            Log.e(tag, msg)
        }
    }

測試demo,資源的路徑

1,進入gift/demoRes/目錄.
2,adb push demoRes /sdcard/alphaVideoGift/
3,build-->run.

注意事項:

demo的可以配置外部json
dataSource.configJsonPath = "/sdcard/alphaVideoGift/xxx.json"
也可以將json數(shù)據(jù)寫入mp4視頻,自定義一個box,播放視頻前解析出json即可以不配置外部json

技術(shù)實現(xiàn),部分代碼

1,頂點、紋理坐標(biāo)array

 val array = FloatArray(8)
private var floatBuffer: FloatBuffer = ByteBuffer
            .allocateDirect(array.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(array)
            
fun setVertexAttribPointer(attributeLocation: Int) {
        if (attributeLocation < 0) {
            EGLUtil.checkGlError("return setVertexAttribPointer $attributeLocation")
            return
        }
        floatBuffer.position(0)
        GLES20.glVertexAttribPointer(attributeLocation, 2, GLES20.GL_FLOAT, false, 0, floatBuffer)
        GLES20.glEnableVertexAttribArray(attributeLocation)
        EGLUtil.checkGlError("setVertexAttribPointer $attributeLocation")
    }

2,設(shè)置坐標(biāo)數(shù)組

  // 頂點坐標(biāo),遮罩渲染rect
        val rect = config.rgbPointRect
        vertexArray.setArray(
                VertexUtil.create(
                        config.renderWidth,
                        config.renderHeight,
                        rect,
                        vertexArray.array
                )
        )
        vertexArray.setVertexAttribPointer(shader.aPositionLocation)

3,頂點坐標(biāo)的轉(zhuǎn)換

    fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray {
        // x0
        array[0] = switchX(rect.x.toFloat() / width)
        // y0
        array[1] = switchY(rect.y.toFloat() / height)
        // x1
        array[2] = switchX(rect.x.toFloat() / width)
        // y1
        array[3] = switchY((rect.y.toFloat() + rect.height) / height)
        // x2
        array[4] = switchX((rect.x.toFloat() + rect.width) / width)
        // y2
        array[5] = switchY(rect.y.toFloat() / height)
        // x3
        array[6] = switchX((rect.x.toFloat() + rect.width) / width)
        // y3
        array[7] = switchY((rect.y.toFloat() + rect.height) / height)
        return array
    }
    
    private fun switchX(x: Float): Float {
        return x * 2f - 1f
    }
    private fun switchY(y: Float): Float {
        return -y * 2f + 1 //((y * 2f - 2f) * -1f) - 1f
    }

4,紋理坐標(biāo)的轉(zhuǎn)換

    fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray {
        // x0
        array[0] = rect.x.toFloat() / width
        // y0
        array[1] = rect.y.toFloat() / height
        // x1
        array[2] = rect.x.toFloat() / width
        // y1
        array[3] = (rect.y.toFloat() + rect.height) / height
        // x2
        array[4] = (rect.x.toFloat() + rect.width) / width
        // y2
        array[5] = rect.y.toFloat() / height
        // x3
        array[6] = (rect.x.toFloat() + rect.width) / width
        // y3
        array[7] = (rect.y.toFloat() + rect.height) / height
        return array
    }

部分融合的頂點、片元著色器

class FinalMixShader {
    companion object {
        private const val NO_POSITION = -1

        private const val VERTEX = """
                attribute vec4 a_Position;  
                attribute vec2 a_TexCoordinateSrc;
                attribute vec4 a_TexCoordinateRgb;
                attribute vec4 a_TexCoordinateAlpha;
                varying vec2 v_TexCoordinateSrc;
                varying vec2 v_TexCoordinateRgb;
                varying vec2 v_TexCoordinateAlpha;
                void main()
                {
                    v_TexCoordinateAlpha = vec2(a_TexCoordinateAlpha.x, a_TexCoordinateAlpha.y);
                    v_TexCoordinateRgb = vec2(a_TexCoordinateRgb.x, a_TexCoordinateRgb.y);
                    v_TexCoordinateSrc = a_TexCoordinateSrc;
                    gl_Position = a_Position;
                }
        """

        private const val FRAGMENT = """
                #extension GL_OES_EGL_image_external : require
                precision mediump float; 
                uniform sampler2D u_TextureSrcUnit;
                uniform samplerExternalOES u_TextureVideoUnit;
                varying vec2 v_TexCoordinateSrc;
                varying vec2 v_TexCoordinateRgb;
                varying vec2 v_TexCoordinateAlpha;
                void main()
                {
                    vec4 alphaColor = texture2D(u_TextureVideoUnit, v_TexCoordinateAlpha);
                    vec4 rgbColor = texture2D(u_TextureVideoUnit, v_TexCoordinateRgb);
                    vec4 srcRgba = texture2D(u_TextureSrcUnit, v_TexCoordinateSrc);
                    if (srcRgba.rgb == vec3(0.0, 0.0, 0.0) || srcRgba.a == 0.0) {
                        gl_FragColor = vec4(rgbColor.rgb,alphaColor.r);
                    } else {
                         mediump float green = alphaColor.g;
                         mediump float percent = green;
                         gl_FragColor = vec4(mix(rgbColor.rgb, srcRgba.rgb, percent), mix(rgbColor.a, green, percent));
                    }
                }
        """
    }
}

部分OpenGL渲染

 // 傳入遮罩紋理和視頻紋理
        if (fboTextureId >= 0) {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)
            GLES20.glUniform1i(shader.uTextureSrcUnitLocation, 0)
        }
        if (mVideoTextureId >= 0) {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
            GLES20.glBindTexture(VideoRender.GL_TEXTURE_EXTERNAL_OES, mVideoTextureId)
            GLES20.glUniform1i(shader.uTextureMaskUnitLocation, 1)
        }

        EGLUtil.enableBlend()
        // draw
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
        EGLUtil.checkGlError("final mix render")

其他說明

本文主要是Android端的實現(xiàn),是技術(shù)預(yù)研階段的設(shè)計思想和某些實現(xiàn),實際應(yīng)用中的會有更多拓展和優(yōu)化。僅供學(xué)習(xí)交流,是在已有開源項目的基礎(chǔ)上的拓展和學(xué)習(xí)。文章如有侵權(quán),可以留言聯(lián)系杜小菜。
早期sdk版本可參看:
https://github.com/duqian291902259/AlphaPlayerPlus

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容