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 怎麼使用,可以參考文章
由於本文是面向初學者快速上手 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