1. 程式人生 > 實用技巧 >FFmpeg 開發(05):FFmpeg + OpenGLES 實現視訊解碼播放和視訊濾鏡

FFmpeg 開發(05):FFmpeg + OpenGLES 實現視訊解碼播放和視訊濾鏡

該原創文章首發於微信公眾號:位元組流動

FFmpeg 開發系列連載:

FFmpeg 開發(01):FFmpeg 編譯和整合
FFmpeg 開發(02):FFmpeg + ANativeWindow 實現視訊解碼播放
FFmpeg 開發(03):FFmpeg + OpenSLES 實現音訊解碼播放
FFmpeg 開發(04):FFmpeg + OpenGLES 實現音訊視覺化播放

前面 Android FFmpeg 開發系列文章中,我們已經利用 FFmpeg 的解碼功能和 ANativeWindow 的渲染功能,實現了的視訊的解碼播放。但是,當你想為播放器做一些視訊濾鏡時,如加水印、旋轉縮放等效果,使用 OpenGL ES 實現起來就極為方便。

視訊解碼播放和視訊濾鏡

OpenGLES 渲染解碼幀

經過上面幾節的介紹,我們對音視訊的解碼過程已經比較熟悉了。本文要用 OpenGL 實現視訊的渲染,這裡再回顧下視訊的解碼流程:

視訊的解碼流程

從流程圖中可以看出,解碼一幀影象後,首先將對影象進行格式轉換,轉換成 RGBA 格式,使用 OpenGL 或 ANativeWindow 可以直接進行渲染。

當然,使用 OpenGL 進行渲染時,為了提升效能,可以將格式轉換放到 GPU 上來做(即 shader 實現 YUV 到 RGB 的轉換),也可以使用 OES 紋理直接接收 YUV 影象資料,這裡就不進行展開講了。

瞭解視訊解碼到渲染的流程之後,我們就可以構建 OpenGL 渲染環境。從之前介紹 EGL 的文章中,我們知道在使用 OpenGL API 之前,必須要先利用 EGL 建立好 OpenGL 的渲染上下文環境。至於 EGL 怎麼使用,可以參考文章

OpenGLES 與 EGL 的關係

由於本文是面向初學者快速上手 FFmpeg 開發,我們直接利用 Android GLSurfaceView 類建立 OpenGL 渲染環境,GLSurfaceView 類已經封裝了 EGL 建立渲染上下文的操作,並啟動了一個獨立的渲染執行緒,完全符合我們渲染視訊解碼幀的需求。

實際上,GLSurfaceView 類在生產開發中可以滿足絕大多數的螢幕渲染場景,一般要實現多執行緒渲染的時候才需要我們單獨操作 EGL 的介面。

那麼,你肯定會有疑問:GLSurfaceView 是 Java 的類,難道要將 Native 層解碼後的視訊影象傳到 Java 層再進行渲染嗎?大可不必,我們只需要將 Java 層的呼叫棧通過 JNI 延伸到 Native 層即可。

GLSurfaceView 類 Renderer 介面對應渲染的三個關鍵函式,我們通過 JNI 延伸到 Native 層:

@Override
publicvoidonSurfaceCreated(GL10gl10,EGLConfigeglConfig){
FFMediaPlayer.native_OnSurfaceCreated();
}

@Override
publicvoidonSurfaceChanged(GL10gl10,intw,inth){
FFMediaPlayer.native_OnSurfaceChanged(w,h);
}

@Override
publicvoidonDrawFrame(GL10gl10){
FFMediaPlayer.native_OnDrawFrame();
}

//forvideoopenGLrender
publicstaticnativevoidnative_OnSurfaceCreated();
publicstaticnativevoidnative_OnSurfaceChanged(intwidth,intheight);
publicstaticnativevoidnative_OnDrawFrame();

然後,我們在 Native 層建立一個 OpenGLRender 類來用來管理 OpenGL 的渲染。

//介面
classVideoRender{
public:
virtual~VideoRender(){}
virtualvoidInit(intvideoWidth,intvideoHeight,int*dstSize)=0;
virtualvoidRenderVideoFrame(NativeImage*pImage)=0;
virtualvoidUnInit()=0;
};

//OpenGLRender類定義
classOpenGLRender:publicVideoRender{
public:
virtualvoidInit(intvideoWidth,intvideoHeight,int*dstSize);
virtualvoidRenderVideoFrame(NativeImage*pImage);
virtualvoidUnInit();

//對應Java層GLSurfaceView.Renderer的三個介面
voidOnSurfaceCreated();
voidOnSurfaceChanged(intw,inth);
voidOnDrawFrame();

//靜態例項管理
staticOpenGLRender*GetInstance();
staticvoidReleaseInstance();

//設定變換矩陣,控制影象的旋轉縮放
voidUpdateMVPMatrix(intangleX,intangleY,floatscaleX,floatscaleY);

private:
OpenGLRender();
virtual~OpenGLRender();

staticstd::mutexm_Mutex;
staticOpenGLRender*s_Instance;
GLuintm_ProgramObj=GL_NONE;
GLuintm_TextureId;
GLuintm_VaoId;
GLuintm_VboIds[3];
NativeImagem_RenderImage;
glm::mat4m_MVPMatrix;//變換矩陣
};

OpenGLRender 類的完整實現。

#include"OpenGLRender.h"
#include<GLUtils.h>
#include<gtc/matrix_transform.hpp>

OpenGLRender*OpenGLRender::s_Instance=nullptr;
std::mutexOpenGLRender::m_Mutex;

staticcharvShaderStr[]=
"#version300es\n"
"layout(location=0)invec4a_position;\n"
"layout(location=1)invec2a_texCoord;\n"
"uniformmat4u_MVPMatrix;\n"
"outvec2v_texCoord;\n"
"voidmain()\n"
"{\n"
"gl_Position=u_MVPMatrix*a_position;\n"
"v_texCoord=a_texCoord;\n"
"}";

staticcharfShaderStr[]=
"#version300es\n"
"precisionhighpfloat;\n"
"invec2v_texCoord;\n"
"layout(location=0)outvec4outColor;\n"
"uniformsampler2Ds_TextureMap;//取樣器\n"
"voidmain()\n"
"{\n"
"outColor=texture(s_TextureMap,v_texCoord);\n"
"}";

GLfloatverticesCoords[]={
-1.0f,1.0f,0.0f,//Position0
-1.0f,-1.0f,0.0f,//Position1
1.0f,-1.0f,0.0f,//Position2
1.0f,1.0f,0.0f,//Position3
};

GLfloattextureCoords[]={
0.0f,0.0f,//TexCoord0
0.0f,1.0f,//TexCoord1
1.0f,1.0f,//TexCoord2
1.0f,0.0f//TexCoord3
};

GLushortindices[]={0,1,2,0,2,3};

OpenGLRender::OpenGLRender(){

}

OpenGLRender::~OpenGLRender(){
//釋放快取影象
NativeImageUtil::FreeNativeImage(&m_RenderImage);

}

//初始化視訊影象的寬和高
voidOpenGLRender::Init(intvideoWidth,intvideoHeight,int*dstSize){
LOGCATE("OpenGLRender::InitRendervideo[w,h]=[%d,%d]",videoWidth,videoHeight);
std::unique_lock<std::mutex>lock(m_Mutex);
m_RenderImage.format=IMAGE_FORMAT_RGBA;
m_RenderImage.width=videoWidth;
m_RenderImage.height=videoHeight;
dstSize[0]=videoWidth;
dstSize[1]=videoHeight;
m_FrameIndex=0;

}

//接收解碼後的視訊幀
voidOpenGLRender::RenderVideoFrame(NativeImage*pImage){
LOGCATE("OpenGLRender::RenderVideoFramepImage=%p",pImage);
if(pImage==nullptr||pImage->ppPlane[0]==nullptr)
return;
//加互斥鎖,解碼執行緒和渲染執行緒是2個不同的執行緒,避免資料訪問衝突
std::unique_lock<std::mutex>lock(m_Mutex);
if(m_RenderImage.ppPlane[0]==nullptr)
{
NativeImageUtil::AllocNativeImage(&m_RenderImage);
}

NativeImageUtil::CopyNativeImage(pImage,&m_RenderImage);
}

voidOpenGLRender::UnInit(){

}

//設定變換矩陣,控制影象的旋轉縮放
voidOpenGLRender::UpdateMVPMatrix(intangleX,intangleY,floatscaleX,floatscaleY)
{
angleX=angleX%360;
angleY=angleY%360;

//轉化為弧度角
floatradiansX=static_cast<float>(MATH_PI/180.0f*angleX);
floatradiansY=static_cast<float>(MATH_PI/180.0f*angleY);
//Projectionmatrix
glm::mat4Projection=glm::ortho(-1.0f,1.0f,-1.0f,1.0f,0.1f,100.0f);
//glm::mat4Projection=glm::frustum(-ratio,ratio,-1.0f,1.0f,4.0f,100.0f);
//glm::mat4Projection=glm::perspective(45.0f,ratio,0.1f,100.f);

//Viewmatrix
glm::mat4View=glm::lookAt(
glm::vec3(0,0,4),//Cameraisat(0,0,1),inWorldSpace
glm::vec3(0,0,0),//andlooksattheorigin
glm::vec3(0,1,0)//Headisup(setto0,-1,0tolookupside-down)
);

//Modelmatrix
glm::mat4Model=glm::mat4(1.0f);
Model=glm::scale(Model,glm::vec3(scaleX,scaleY,1.0f));
Model=glm::rotate(Model,radiansX,glm::vec3(1.0f,0.0f,0.0f));
Model=glm::rotate(Model,radiansY,glm::vec3(0.0f,1.0f,0.0f));
Model=glm::translate(Model,glm::vec3(0.0f,0.0f,0.0f));

m_MVPMatrix=Projection*View*Model;

}

voidOpenGLRender::OnSurfaceCreated(){
LOGCATE("OpenGLRender::OnSurfaceCreated");

m_ProgramObj=GLUtils::CreateProgram(vShaderStr,fShaderStr);
if(!m_ProgramObj)
{
LOGCATE("OpenGLRender::OnSurfaceCreatedcreateprogramfail");
return;
}

glGenTextures(1,&m_TextureId);
glBindTexture(GL_TEXTURE_2D,m_TextureId);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glBindTexture(GL_TEXTURE_2D,GL_NONE);

//GenerateVBOIdsandloadtheVBOswithdata
glGenBuffers(3,m_VboIds);
glBindBuffer(GL_ARRAY_BUFFER,m_VboIds[0]);
glBufferData(GL_ARRAY_BUFFER,sizeof(verticesCoords),verticesCoords,GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER,m_VboIds[1]);
glBufferData(GL_ARRAY_BUFFER,sizeof(textureCoords),textureCoords,GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,m_VboIds[2]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);

//GenerateVAOId
glGenVertexArrays(1,&m_VaoId);
glBindVertexArray(m_VaoId);

glBindBuffer(GL_ARRAY_BUFFER,m_VboIds[0]);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(GLfloat),(constvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER,GL_NONE);

glBindBuffer(GL_ARRAY_BUFFER,m_VboIds[1]);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,2*sizeof(GLfloat),(constvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER,GL_NONE);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,m_VboIds[2]);

glBindVertexArray(GL_NONE);

UpdateMVPMatrix(0,0,1.0f,1.0f);
}

voidOpenGLRender::OnSurfaceChanged(intw,inth){
LOGCATE("OpenGLRender::OnSurfaceChanged[w,h]=[%d,%d]",w,h);
m_ScreenSize.x=w;
m_ScreenSize.y=h;
glViewport(0,0,w,h);
glClearColor(1.0f,1.0f,1.0f,1.0f);
}

voidOpenGLRender::OnDrawFrame(){
glClear(GL_COLOR_BUFFER_BIT);
if(m_ProgramObj==GL_NONE||m_TextureId==GL_NONE||m_RenderImage.ppPlane[0]==nullptr)return;
LOGCATE("OpenGLRender::OnDrawFrame[w,h]=[%d,%d]",m_RenderImage.width,m_RenderImage.height);
m_FrameIndex++;

//uploadRGBAimagedata
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,m_TextureId);

//加互斥鎖,解碼執行緒和渲染執行緒是2個不同的執行緒,避免資料訪問衝突
std::unique_lock<std::mutex>lock(m_Mutex);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,m_RenderImage.width,m_RenderImage.height,0,GL_RGBA,GL_UNSIGNED_BYTE,m_RenderImage.ppPlane[0]);
lock.unlock();

glBindTexture(GL_TEXTURE_2D,GL_NONE);

//Usetheprogramobject
glUseProgram(m_ProgramObj);

glBindVertexArray(m_VaoId);

GLUtils::setMat4(m_ProgramObj,"u_MVPMatrix",m_MVPMatrix);

//BindtheRGBAmap
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,m_TextureId);
GLUtils::setFloat(m_ProgramObj,"s_TextureMap",0);

glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,(constvoid*)0);

}

//單例模式,全域性只有一個OpenGLRender
OpenGLRender*OpenGLRender::GetInstance(){
if(s_Instance==nullptr)
{
std::lock_guard<std::mutex>lock(m_Mutex);
if(s_Instance==nullptr)
{
s_Instance=newOpenGLRender();
}

}
returns_Instance;
}

//釋放靜態例項
voidOpenGLRender::ReleaseInstance(){
if(s_Instance!=nullptr)
{
std::lock_guard<std::mutex>lock(m_Mutex);
if(s_Instance!=nullptr)
{
deletes_Instance;
s_Instance=nullptr;
}

}
}

OpenGLRender 在 JNI 層的呼叫。

JNIEXPORTvoidJNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnSurfaceCreated(JNIEnv*env,
jclassclazz)
{
OpenGLRender::GetInstance()->OnSurfaceCreated();
}

JNIEXPORTvoidJNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnSurfaceChanged(JNIEnv*env,
jclassclazz,jintwidth,
jintheight)
{
OpenGLRender::GetInstance()->OnSurfaceChanged(width,height);
}

JNIEXPORTvoidJNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnDrawFrame(JNIEnv*env,jclassclazz)
{
OpenGLRender::GetInstance()->OnDrawFrame();
}
渲染效果

新增簡單的視訊濾鏡

這裡又回到了 OpenGL ES 開發領域,對這一塊感興趣的同學可以參考這篇Android OpenGL ES 從入門到精通系統性學習教程

利用 OpenGL 實現好視訊的渲染之後,可以很方便地利用 shader 新增你想要的視訊濾鏡,這裡我們直接可以參考相機濾鏡的實現。

黑白濾鏡

我們將輸出視訊幀的一半渲染成經典黑白風格的影象,實現的 shader 如下:

//黑白濾鏡
#version300es
precisionhighpfloat;
invec2v_texCoord;
layout(location=0)outvec4outColor;
uniformsampler2Ds_TextureMap;//取樣器
voidmain()
{
outColor=texture(s_TextureMap,v_texCoord);
if(v_texCoord.x>0.5)//將輸出視訊幀的一半渲染成經典黑白風格的影象
outColor=vec4(vec3(outColor.r*0.299+outColor.g*0.587+outColor.b*0.114),outColor.a);
}

黑白濾鏡的呈現效果:

黑白濾鏡

動態網格

動態網格濾鏡是將視訊影象分成規則的網格,動態修改網格的邊框寬度,實現的 shader 如下:

//dynimicmesh動態網格
#version300es
precisionhighpfloat;
invec2v_texCoord;
layout(location=0)outvec4outColor;
uniformsampler2Ds_TextureMap;//取樣器
uniformfloatu_Offset;
uniformvec2u_TexSize;
voidmain()
{
vec2imgTexCoord=v_texCoord*u_TexSize;
floatsideLength=u_TexSize.y/6.0;
floatmaxOffset=0.15*sideLength;
floatx=mod(imgTexCoord.x,floor(sideLength));
floaty=mod(imgTexCoord.y,floor(sideLength));

floatoffset=u_Offset*maxOffset;

if(offset<=x
&&x<=sideLength-offset
&&offset<=y
&&y<=sideLength-offset)
{
outColor=texture(s_TextureMap,v_texCoord);
}
else
{
outColor=vec4(1.0,1.0,1.0,1.0);
}
}

動態網格濾鏡的渲染過程:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,m_TextureId);

std::unique_lock<std::mutex>lock(m_Mutex);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,m_RenderImage.width,m_RenderImage.height,0,GL_RGBA,GL_UNSIGNED_BYTE,m_RenderImage.ppPlane[0]);
lock.unlock();

glBindTexture(GL_TEXTURE_2D,GL_NONE);

//指定著色器程式
glUseProgram(m_ProgramObj);

//繫結VAO
glBindVertexArray(m_VaoId);

//傳入變換矩陣
GLUtils::setMat4(m_ProgramObj,"u_MVPMatrix",m_MVPMatrix);

//繫結紋理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,m_TextureId);
GLUtils::setFloat(m_ProgramObj,"s_TextureMap",0);

//設定偏移量
floatoffset=(sin(m_FrameIndex*MATH_PI/25)+1.0f)/2.0f;
GLUtils::setFloat(m_ProgramObj,"u_Offset",offset);

//設定影象尺寸
GLUtils::setVec2(m_ProgramObj,"u_TexSize",vec2(m_RenderImage.width,m_RenderImage.height));

glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,(constvoid*)0);

動態網格濾鏡的呈現效果:

動態網格濾鏡

縮放和旋轉

我們在 GLSurfaceView 監聽使用者的滑動和縮放手勢,控制 OpenGLRender 的變換矩陣,從而實現視訊影象的旋轉和縮放。

視訊影象的旋轉和縮放

聯絡與交流

技術交流/獲取原始碼可以新增我的微信:Byte-Flow