作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/108737936 FFmpeg 開發系列連載:- FFmpeg 開發(01):FFmpeg 編譯和內建
- FFmpeg 開發(02):FFmpeg + ANativeWindow 實作視訊解碼播放
- FFmpeg 開發(03):FFmpeg + OpenSLES 實作音頻解碼播放
- FFmpeg 開發(04):FFmpeg + OpenGLES 實作音頻可視化播放
- FFmpeg 開發(05):FFmpeg + OpenGLES 實作視訊解碼播放和視訊濾鏡
- FFmpeg 開發(06):FFmpeg 播放器實作音視訊同步的三種方式
- FFmpeg 開發(07):FFmpeg + OpenGLES 實作 3D 全景播放器
前文中,我們已經利用 FFmpeg + OpenGLES + OpenSLES 實作了一個多媒體播放器,本文将在視訊渲染方面對播放器進行優化。
視訊渲染優化
前文中,我們都是将解碼的視訊幀通過 swscale 庫轉換為 RGBA 格式,然後在送給 OpenGL 渲染,而視訊幀通常的格式是 YUV420P/YUV420SP ,是以大部分情況下都需要 swscale 進行格式轉換。
當視訊尺寸比較大時,再用 swscale 進行格式轉化的話,就會存在性能瓶頸,是以本文将 YUV 到 RGBA 的格式轉換放到 shader 裡,用 GPU 來實作格式轉換,提升渲染效率。

本文視訊渲染優化,實質上是對 OpenGLRender 視訊渲染器進行改進,使其支援 YUV420P 、 NV21 以及 NV12 這些常用格式圖像的渲染。
我們在前文
一文掌握 YUV 的圖像處理中知道,YUV420P 格式的圖像在記憶體中有 3 個平面,YUV420SP (NV21、NV12)格式的圖像在記憶體中有 2 個平面,而 RGBA 格式的圖像隻有一個平面。
是以,OpenGLRender 視訊渲染器要相容 YUV420P、 YUV420SP 以及 RGBA 格式,需要建立 3 個紋理存儲待渲染的資料,渲染 YUV420P 格式的圖像需要用到 3 個紋理,渲染 YUV420SP 格式的圖像隻需用到 2 個紋理即可,而渲染 RGBA 格式圖像隻需一個紋理。
判斷解碼後視訊幀的格式,AVFrame 是解碼後的視訊幀。
void VideoDecoder::OnFrameAvailable(AVFrame *frame) {
LOGCATE("VideoDecoder::OnFrameAvailable frame=%p", frame);
if(m_VideoRender != nullptr && frame != nullptr) {
NativeImage image;
//YUV420P
if(GetCodecContext()->pix_fmt == AV_PIX_FMT_YUV420P || GetCodecContext()->pix_fmt == AV_PIX_FMT_YUVJ420P) {
image.format = IMAGE_FORMAT_I420;
image.width = frame->width;
image.height = frame->height;
image.pLineSize[0] = frame->linesize[0];
image.pLineSize[1] = frame->linesize[1];
image.pLineSize[2] = frame->linesize[2];
image.ppPlane[0] = frame->data[0];
image.ppPlane[1] = frame->data[1];
image.ppPlane[2] = frame->data[2];
if(frame->data[0] && frame->data[1] && !frame->data[2] && frame->linesize[0] == frame->linesize[1] && frame->linesize[2] == 0) {
// on some android device, output of h264 mediacodec decoder is NV12 相容某些裝置可能出現的格式不比對問題
image.format = IMAGE_FORMAT_NV12;
}
} else if (GetCodecContext()->pix_fmt == AV_PIX_FMT_NV12) { //NV12
image.format = IMAGE_FORMAT_NV12;
image.width = frame->width;
image.height = frame->height;
image.pLineSize[0] = frame->linesize[0];
image.pLineSize[1] = frame->linesize[1];
image.ppPlane[0] = frame->data[0];
image.ppPlane[1] = frame->data[1];
} else if (GetCodecContext()->pix_fmt == AV_PIX_FMT_NV21) { //NV21
image.format = IMAGE_FORMAT_NV21;
image.width = frame->width;
image.height = frame->height;
image.pLineSize[0] = frame->linesize[0];
image.pLineSize[1] = frame->linesize[1];
image.ppPlane[0] = frame->data[0];
image.ppPlane[1] = frame->data[1];
} else if (GetCodecContext()->pix_fmt == AV_PIX_FMT_RGBA) { //RGBA
image.format = IMAGE_FORMAT_RGBA;
image.width = frame->width;
image.height = frame->height;
image.pLineSize[0] = frame->linesize[0];
image.ppPlane[0] = frame->data[0];
} else { //其他格式由 swscale 轉換為 RGBA
sws_scale(m_SwsContext, frame->data, frame->linesize, 0,
m_VideoHeight, m_RGBAFrame->data, m_RGBAFrame->linesize);
image.format = IMAGE_FORMAT_RGBA;
image.width = m_RenderWidth;
image.height = m_RenderHeight;
image.ppPlane[0] = m_RGBAFrame->data[0];
}
//将圖像傳遞給渲染器進行渲染
m_VideoRender->RenderVideoFrame(&image);
}
}
建立 3 個紋理,但是不指定要加載圖像的格式。
// TEXTURE_NUM = 3
glGenTextures(TEXTURE_NUM, m_TextureIds);
for (int i = 0; i < TEXTURE_NUM ; ++i) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[i]);
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);
}
加載不同格式的資料到紋理。
switch (m_RenderImage.format)
{
//加載 RGBA 類型的資料
case IMAGE_FORMAT_RGBA:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
break;
//加載 YUV420SP 類型的資料
case IMAGE_FORMAT_NV21:
case IMAGE_FORMAT_NV12:
//upload Y plane data
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_RenderImage.width,
m_RenderImage.height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
//update UV plane data
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, m_RenderImage.width >> 1,
m_RenderImage.height >> 1, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
m_RenderImage.ppPlane[1]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
break;
//加載 YUV420P 類型的資料
case IMAGE_FORMAT_I420:
//upload Y plane data
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_RenderImage.width,
m_RenderImage.height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
//update U plane data
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_RenderImage.width >> 1,
m_RenderImage.height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
m_RenderImage.ppPlane[1]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
//update V plane data
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[2]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_RenderImage.width >> 1,
m_RenderImage.height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
m_RenderImage.ppPlane[2]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
break;
default:
break;
}
對應的頂點着色器和片段着色器,其中重要的是,片段着色器需要針對不同的圖像格式采用不用的采樣政策。
//頂點着色器
#version 300 es
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
void main()
{
gl_Position = a_Position;
v_texCoord = a_texCoord;
}
//片段着色器
#version 300 es
precision highp float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_texture0;
uniform sampler2D s_texture1;
uniform sampler2D s_texture2;
uniform int u_ImgType;// 1:RGBA, 2:NV21, 3:NV12, 4:I420
void main()
{
if(u_ImgType == 1) //RGBA
{
outColor = texture(s_texture0, v_texCoord);
}
else if(u_ImgType == 2) //NV21
{
vec3 yuv;
yuv.x = texture(s_texture0, v_texCoord).r;
yuv.y = texture(s_texture1, v_texCoord).a - 0.5;
yuv.z = texture(s_texture1, v_texCoord).r - 0.5;
highp vec3 rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.344, 1.770,
1.403, -0.714, 0.0) * yuv;
outColor = vec4(rgb, 1.0);
}
else if(u_ImgType == 3) //NV12
{
vec3 yuv;
yuv.x = texture(s_texture0, v_texCoord).r;
yuv.y = texture(s_texture1, v_texCoord).r - 0.5;
yuv.z = texture(s_texture1, v_texCoord).a - 0.5;
highp vec3 rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.344, 1.770,
1.403, -0.714, 0.0) * yuv;
outColor = vec4(rgb, 1.0);
}
else if(u_ImgType == 4) //I420
{
vec3 yuv;
yuv.x = texture(s_texture0, v_texCoord).r;
yuv.y = texture(s_texture1, v_texCoord).r - 0.5;
yuv.z = texture(s_texture2, v_texCoord).r - 0.5;
highp vec3 rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.344, 1.770,
1.403, -0.714, 0.0) * yuv;
outColor = vec4(rgb, 1.0);
}
else
{
outColor = vec4(1.0);
}
}
其中片段着色器 u_ImgType 變量用于設定待渲染圖像的格式類型,進而采用不同的采樣轉換政策。
需要注意的是,YUV 格式圖像 UV 分量的預設值分别是 127 ,Y 分量預設值是 0 ,8 個 bit 位的取值範圍是 0 ~ 255,由于在 shader 中紋理采樣值需要進行歸一化,是以 UV 分量的采樣值需要分别減去 0.5 ,確定 YUV 到 RGB 正确轉換。
聯系與交流
技術交流/擷取源碼可以添加我的微信:Byte-Flow
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。