该原创文章首发于微信公众号:字节流动
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 层:
1 @Override 2 public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { 3 FFMediaPlayer.native_OnSurfaceCreated(); 4 } 5 6 @Override 7 public void onSurfaceChanged(GL10 gl10, int w, int h) { 8 FFMediaPlayer.native_OnSurfaceChanged(w, h); 9 } 10 11 @Override 12 public void onDrawFrame(GL10 gl10) { 13 FFMediaPlayer.native_OnDrawFrame(); 14 } 15 16 //for video openGL render 17 public static native void native_OnSurfaceCreated(); 18 public static native void native_OnSurfaceChanged(int width, int height); 19 public static native void native_OnDrawFrame();
然后,我们在 Native 层创建一个 OpenGLRender 类来用来管理 OpenGL 的渲染。
1//接口 2class VideoRender { 3public: 4 virtual ~VideoRender(){} 5 virtual void Init(int videoWidth, int videoHeight, int *dstSize) = 0; 6 virtual void RenderVideoFrame(NativeImage *pImage) = 0; 7 virtual void UnInit() = 0; 8}; 9 10//OpenGLRender 类定义 11class OpenGLRender: public VideoRender{ 12public: 13 virtual void Init(int videoWidth, int videoHeight, int *dstSize); 14 virtual void RenderVideoFrame(NativeImage *pImage); 15 virtual void UnInit(); 16 17 //对应 Java 层 GLSurfaceView.Renderer 的三个接口 18 void OnSurfaceCreated(); 19 void OnSurfaceChanged(int w, int h); 20 void OnDrawFrame(); 21 22 //静态实例管理 23 static OpenGLRender *GetInstance(); 24 static void ReleaseInstance(); 25 26 //设置变换矩阵,控制图像的旋转缩放 27 void UpdateMVPMatrix(int angleX, int angleY, float scaleX, float scaleY); 28 29private: 30 OpenGLRender(); 31 virtual ~OpenGLRender(); 32 33 static std::mutex m_Mutex; 34 static OpenGLRender* s_Instance; 35 GLuint m_ProgramObj = GL_NONE; 36 GLuint m_TextureId; 37 GLuint m_VaoId; 38 GLuint m_VboIds[3]; 39 NativeImage m_RenderImage; 40 glm::mat4 m_MVPMatrix;//变换矩阵 41};
OpenGLRender 类的完整实现。
1#include "OpenGLRender.h" 2#include <GLUtils.h> 3#include <gtc/matrix_transform.hpp> 4 5OpenGLRender* OpenGLRender::s_Instance = nullptr; 6std::mutex OpenGLRender::m_Mutex; 7 8static char vShaderStr[] = 9 "#version 300 es\n" 10 "layout(location = 0) in vec4 a_position;\n" 11 "layout(location = 1) in vec2 a_texCoord;\n" 12 "uniform mat4 u_MVPMatrix;\n" 13 "out vec2 v_texCoord;\n" 14 "void main()\n" 15 "{\n" 16 " gl_Position = u_MVPMatrix * a_position;\n" 17 " v_texCoord = a_texCoord;\n" 18 "}"; 19 20static char fShaderStr[] = 21 "#version 300 es\n" 22 "precision highp float;\n" 23 "in vec2 v_texCoord;\n" 24 "layout(location = 0) out vec4 outColor;\n" 25 "uniform sampler2D s_TextureMap;//采样器\n" 26 "void main()\n" 27 "{\n" 28 " outColor = texture(s_TextureMap, v_texCoord);\n" 29 "}"; 30 31GLfloat verticesCoords[] = { 32 -1.0f, 1.0f, 0.0f, // Position 0 33 -1.0f, -1.0f, 0.0f, // Position 1 34 1.0f, -1.0f, 0.0f, // Position 2 35 1.0f, 1.0f, 0.0f, // Position 3 36}; 37 38GLfloat textureCoords[] = { 39 0.0f, 0.0f, // TexCoord 0 40 0.0f, 1.0f, // TexCoord 1 41 1.0f, 1.0f, // TexCoord 2 42 1.0f, 0.0f // TexCoord 3 43}; 44 45GLushort indices[] = { 0, 1, 2, 0, 2, 3 }; 46 47OpenGLRender::OpenGLRender() { 48 49} 50 51OpenGLRender::~OpenGLRender() { 52 // 释放缓存图像 53 NativeImageUtil::FreeNativeImage(&m_RenderImage); 54 55} 56 57//初始化视频图像的宽和高 58void OpenGLRender::Init(int videoWidth, int videoHeight, int *dstSize) { 59 LOGCATE("OpenGLRender::InitRender video[w, h]=[%d, %d]", videoWidth, videoHeight); 60 std::unique_lock<std::mutex> lock(m_Mutex); 61 m_RenderImage.format = IMAGE_FORMAT_RGBA; 62 m_RenderImage.width = videoWidth; 63 m_RenderImage.height = videoHeight; 64 dstSize[0] = videoWidth; 65 dstSize[1] = videoHeight; 66 m_FrameIndex = 0; 67 68} 69 70// 接收解码后的视频帧 71void OpenGLRender::RenderVideoFrame(NativeImage *pImage) { 72 LOGCATE("OpenGLRender::RenderVideoFrame pImage=%p", pImage); 73 if(pImage == nullptr || pImage->ppPlane[0] == nullptr) 74 return; 75 //加互斥锁,解码线程和渲染线程是 2 个不同的线程,避免数据访问冲突 76 std::unique_lock<std::mutex> lock(m_Mutex); 77 if(m_RenderImage.ppPlane[0] == nullptr) 78 { 79 NativeImageUtil::AllocNativeImage(&m_RenderImage); 80 } 81 82 NativeImageUtil::CopyNativeImage(pImage, &m_RenderImage); 83} 84 85void OpenGLRender::UnInit() { 86 87} 88 89// 设置变换矩阵,控制图像的旋转缩放 90void OpenGLRender::UpdateMVPMatrix(int angleX, int angleY, float scaleX, float scaleY) 91{ 92 angleX = angleX % 360; 93 angleY = angleY % 360; 94 95 //转化为弧度角 96 float radiansX = static_cast<float>(MATH_PI / 180.0f * angleX); 97 float radiansY = static_cast<float>(MATH_PI / 180.0f * angleY); 98 // Projection matrix 99 glm::mat4 Projection = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, 0.1f, 100.0f); 100 //glm::mat4 Projection = glm::frustum(-ratio, ratio, -1.0f, 1.0f, 4.0f, 100.0f); 101 //glm::mat4 Projection = glm::perspective(45.0f,ratio, 0.1f,100.f); 102 103 // View matrix 104 glm::mat4 View = glm::lookAt( 105 glm::vec3(0, 0, 4), // Camera is at (0,0,1), in World Space 106 glm::vec3(0, 0, 0), // and looks at the origin 107 glm::vec3(0, 1, 0) // Head is up (set to 0,-1,0 to look upside-down) 108 ); 109 110 // Model matrix 111 glm::mat4 Model = glm::mat4(1.0f); 112 Model = glm::scale(Model, glm::vec3(scaleX, scaleY, 1.0f)); 113 Model = glm::rotate(Model, radiansX, glm::vec3(1.0f, 0.0f, 0.0f)); 114 Model = glm::rotate(Model, radiansY, glm::vec3(0.0f, 1.0f, 0.0f)); 115 Model = glm::translate(Model, glm::vec3(0.0f, 0.0f, 0.0f)); 116 117 m_MVPMatrix = Projection * View * Model; 118 119} 120 121void OpenGLRender::OnSurfaceCreated() { 122 LOGCATE("OpenGLRender::OnSurfaceCreated"); 123 124 m_ProgramObj = GLUtils::CreateProgram(vShaderStr, fShaderStr); 125 if (!m_ProgramObj) 126 { 127 LOGCATE("OpenGLRender::OnSurfaceCreated create program fail"); 128 return; 129 } 130 131 glGenTextures(1, &m_TextureId); 132 glBindTexture(GL_TEXTURE_2D, m_TextureId); 133 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 134 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 135 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 136 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 137 glBindTexture(GL_TEXTURE_2D, GL_NONE); 138 139 // Generate VBO Ids and load the VBOs with data 140 glGenBuffers(3, m_VboIds); 141 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); 142 glBufferData(GL_ARRAY_BUFFER, sizeof(verticesCoords), verticesCoords, GL_STATIC_DRAW); 143 144 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]); 145 glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoords), textureCoords, GL_STATIC_DRAW); 146 147 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[2]); 148 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 149 150 // Generate VAO Id 151 glGenVertexArrays(1, &m_VaoId); 152 glBindVertexArray(m_VaoId); 153 154 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); 155 glEnableVertexAttribArray(0); 156 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (const void *)0); 157 glBindBuffer(GL_ARRAY_BUFFER, GL_NONE); 158 159 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]); 160 glEnableVertexAttribArray(1); 161 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (const void *)0); 162 glBindBuffer(GL_ARRAY_BUFFER, GL_NONE); 163 164 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[2]); 165 166 glBindVertexArray(GL_NONE); 167 168 UpdateMVPMatrix(0, 0, 1.0f, 1.0f); 169} 170 171void OpenGLRender::OnSurfaceChanged(int w, int h) { 172 LOGCATE("OpenGLRender::OnSurfaceChanged [w, h]=[%d, %d]", w, h); 173 m_ScreenSize.x = w; 174 m_ScreenSize.y = h; 175 glViewport(0, 0, w, h); 176 glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 177} 178 179void OpenGLRender::OnDrawFrame() { 180 glClear(GL_COLOR_BUFFER_BIT); 181 if(m_ProgramObj == GL_NONE || m_TextureId == GL_NONE || m_RenderImage.ppPlane[0] == nullptr) return; 182 LOGCATE("OpenGLRender::OnDrawFrame [w, h]=[%d, %d]", m_RenderImage.width, m_RenderImage.height); 183 m_FrameIndex++; 184 185 //upload RGBA image data 186 glActiveTexture(GL_TEXTURE0); 187 glBindTexture(GL_TEXTURE_2D, m_TextureId); 188 189 //加互斥锁,解码线程和渲染线程是 2 个不同的线程,避免数据访问冲突 190 std::unique_lock<std::mutex> lock(m_Mutex); 191 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]); 192 lock.unlock(); 193 194 glBindTexture(GL_TEXTURE_2D, GL_NONE); 195 196 // Use the program object 197 glUseProgram (m_ProgramObj); 198 199 glBindVertexArray(m_VaoId); 200 201 GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix); 202 203 // Bind the RGBA map 204 glActiveTexture(GL_TEXTURE0); 205 glBindTexture(GL_TEXTURE_2D, m_TextureId); 206 GLUtils::setFloat(m_ProgramObj, "s_TextureMap", 0); 207 208 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); 209 210} 211 212// 单例模式,全局只有一个 OpenGLRender 213OpenGLRender *OpenGLRender::GetInstance() { 214 if(s_Instance == nullptr) 215 { 216 std::lock_guard<std::mutex> lock(m_Mutex); 217 if(s_Instance == nullptr) 218 { 219 s_Instance = new OpenGLRender(); 220 } 221 222 } 223 return s_Instance; 224} 225 226// 释放静态实例 227void OpenGLRender::ReleaseInstance() { 228 if(s_Instance != nullptr) 229 { 230 std::lock_guard<std::mutex> lock(m_Mutex); 231 if(s_Instance != nullptr) 232 { 233 delete s_Instance; 234 s_Instance = nullptr; 235 } 236 237 } 238}
OpenGLRender 在 JNI 层的调用。
1JNIEXPORT void JNICALL 2Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnSurfaceCreated(JNIEnv *env, 3 jclass clazz) { 4 OpenGLRender::GetInstance()->OnSurfaceCreated(); 5} 6 7JNIEXPORT void JNICALL 8Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnSurfaceChanged(JNIEnv *env, 9 jclass clazz, jint width, 10 jint height) { 11 OpenGLRender::GetInstance()->OnSurfaceChanged(width, height); 12} 13 14JNIEXPORT void JNICALL 15Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnDrawFrame(JNIEnv *env, jclass clazz) { 16 OpenGLRender::GetInstance()->OnDrawFrame(); 17}

添加简单的视频滤镜
这里又回到了 OpenGL ES 开发领域,对这一块感兴趣的同学可以参考这篇Android OpenGL ES 从入门到精通系统性学习教程。
利用 OpenGL 实现好视频的渲染之后,可以很方便地利用 shader 添加你想要的视频滤镜,这里我们直接可以参考相机滤镜的实现。
黑白滤镜
我们将输出视频帧的一半渲染成经典黑白风格的图像,实现的 shader 如下:
1//黑白滤镜 2#version 300 es 3precision highp float; 4in vec2 v_texCoord; 5layout(location = 0) out vec4 outColor; 6uniform sampler2D s_TextureMap;//采样器 7void main() 8{ 9 outColor = texture(s_TextureMap, v_texCoord); 10 if(v_texCoord.x > 0.5) //将输出视频帧的一半渲染成经典黑白风格的图像 11 outColor = vec4(vec3(outColor.r*0.299 + outColor.g*0.587 + outColor.b*0.114), outColor.a); 12}
黑白滤镜的呈现效果:

动态网格
动态网格滤镜是将视频图像分成规则的网格,动态修改网格的边框宽度,实现的 shader 如下:
1//dynimic mesh 动态网格 2#version 300 es 3precision highp float; 4in vec2 v_texCoord; 5layout(location = 0) out vec4 outColor; 6uniform sampler2D s_TextureMap;//采样器 7uniform float u_Offset; 8uniform vec2 u_TexSize; 9void main() 10{ 11 vec2 imgTexCoord = v_texCoord * u_TexSize; 12 float sideLength = u_TexSize.y / 6.0; 13 float maxOffset = 0.15 * sideLength; 14 float x = mod(imgTexCoord.x, floor(sideLength)); 15 float y = mod(imgTexCoord.y, floor(sideLength)); 16 17 float offset = u_Offset * maxOffset; 18 19 if(offset <= x 20 && x <= sideLength - offset 21 && offset <= y 22 && y <= sideLength - offset) 23 { 24 outColor = texture(s_TextureMap, v_texCoord); 25 } 26 else 27 { 28 outColor = vec4(1.0, 1.0, 1.0, 1.0); 29 } 30}
动态网格滤镜的渲染过程:
1glActiveTexture(GL_TEXTURE0); 2glBindTexture(GL_TEXTURE_2D, m_TextureId); 3 4std::unique_lock<std::mutex> lock(m_Mutex); 5glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]); 6lock.unlock(); 7 8glBindTexture(GL_TEXTURE_2D, GL_NONE); 9 10//指定着色器程序 11glUseProgram (m_ProgramObj); 12 13//绑定 VAO 14glBindVertexArray(m_VaoId); 15 16//传入变换矩阵 17GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix); 18 19//绑定纹理 20glActiveTexture(GL_TEXTURE0); 21glBindTexture(GL_TEXTURE_2D, m_TextureId); 22GLUtils::setFloat(m_ProgramObj, "s_TextureMap", 0); 23 24//设置偏移量 25float offset = (sin(m_FrameIndex * MATH_PI / 25) + 1.0f) / 2.0f; 26GLUtils::setFloat(m_ProgramObj, "u_Offset", offset); 27 28//设置图像尺寸 29GLUtils::setVec2(m_ProgramObj, "u_TexSize", vec2(m_RenderImage.width, m_RenderImage.height)); 30 31glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
动态网格滤镜的呈现效果:

缩放和旋转
我们在 GLSurfaceView 监听用户的滑动和缩放手势,控制 OpenGLRender 的变换矩阵,从而实现视频图像的旋转和缩放。

联系与交流
技术交流/获取源码可以添加我的微信:Byte-Flow
