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值,進行著色渲染,得到帶透明度的特效紋理,如下圖所示:

視頻融合遮罩素材
通常特效中的融合區(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)體系,繪制步驟和基本原理。具體可以參看項目代碼。
流程圖

實現(xiàn)的效果


接入說明
可以參看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