FFmpeg入门教程10.08:软解并使用QOpenGL播放视频(YUV420P)

系列索引:FFmpeg入门系列索引

上一篇:FFmpeg入门教程10.07:软解并使用QtWidget播放视频(YUV420P->RGB32)

我在开发过程中Qt用的比较大,所以本系列主要界面由Qt开发。而Qt主要的特性是跨平台,在嵌入式平台中,主要使用QML进行界面开发,如果使用QML开发视频的话,就需要用到OpenGL了。

本篇主要介绍常用的桌面版的QOpenGL的视频显示,桌面版解码的YUV数据格式为YUV420P,下一篇介绍QML版的视频显示,也是YUV420P格式的。

解码流程图为:

flowchart TB
    F --Yes--> I
    K --下一帧--> F
    I --No--> F
    subgraph init
        direction TB
        A(开始) --> B[打开文件]
        B --> C[查找流信息]
        C --> D[查找对应解码器]
        D --> E[打开解码器]
        E --> F{读取帧}
        F --No--> G(结束)
    end
    subgraph decode
        direction TB
        I{是视频帧?} --Yes--> J[发送帧给解码器]
        J --> K[从解码器获取结果]
    end
flowchart TB
    F --Yes--> I
    K --Next Frame--> F
    I --No--> F
    subgraph init
        direction TB
        A(Start) --> B[avformat_open_input]
        B --> C[av_find_stream_info]
        C --> D[avcodec_find_decoder]
        D --> E[avcodec_open]
        E --> F{av_read_frame}
        F --No--> G(End)
    end
    subgraph decode
        direction TB
        I{Video Packet?} --Yes--> J[avcodec_send_packet]
        J --> K[avcodec_receive_frame]
    end

Qt显示流程图:

flowchart TB

A(开始) --> B[initializalGL初始化QOpenGL]
B --> C[界面缩放resizeGL]
C --> D[纹理绘制paintGL]
D --> E(结束)

函数调用显示流程图为:

flowchart TB

    K --signals/slots--> O
    QOpenGLWidget --prompt--> QWidget
    subgraph decode
        direction TB
        A(Start) --> B[avformat_open_input]
        B --> C[av_find_stream_info]
        C --> D[avcodec_find_decoder]
        D --> E[avcodec_open]
        E --> F{av_read_frame}
        F --No--> G(End)
        F --Yes--> H[avcodec_send_packet]
        H --> I[avcodec_receive_frame]
        I --> J[sws_scale]
        J --> K[QImage]
    end
    subgraph QOpenGLWidget
        direction TB
        N[initGL]
        O[paintGL]
        L[resizeGL]
    end
    subgraph QWidget

    M[QOpenGLWidget]
    end

共分为三个部分。

第一部分:视频解码部分

这个就比较简单,前面几篇文章一直在说这个,FFmpeg默认软解输出格式为YUV420P。

这里需要注意的是解码之后的AVFrame处理。

YUV420P共有三个分量,即:Y、U、V。存储在三个数组中。

解码部分代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
while(av_read_frame(fmtCtx,pkt)>=0){
if(pkt->stream_index == videoStreamIndex){
if(avcodec_send_packet(videoCodecCtx,pkt)>=0){
int ret;
while((ret=avcodec_receive_frame(videoCodecCtx,yuvFrame))>=0){
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during decoding\n");
continue;
}

if(isFirst){
isFirst=false;
emit sigFirst(out_buffer,w,h);
}

int bytes =0;
//Y分量部分
for(int i=0;i<h;i++){
memcpy(out_buffer+bytes,yuvFrame->data[0]+yuvFrame->linesize[0]*i,w);
bytes+=w;
}

int u=h>>1;
//U分量部分
for(int i=0;i<u;i++){
memcpy(out_buffer+bytes,yuvFrame->data[1]+yuvFrame->linesize[1]*i,w/2);
bytes+=w/2;
}

//V分量部分
for(int i=0;i<u;i++){
memcpy(out_buffer+bytes,yuvFrame->data[2]+yuvFrame->linesize[2]*i,w/2);
bytes+=w/2;
}

emit newFrame();

QThread::msleep(24);
}
}
av_packet_unref(pkt);
}
}

将AVFrame三个分量数组的数据按照YUV420P的分量排列格式复制到out_buffer指针指向的内存中。然后发送信号给界面,通知界面一帧解码完成,可以刷新显示界面了。

第二部分:QOpenGL渲染纹理部分

在PC上使用QOpenGL绘制图像,需要继承QOpenGLWidget类,这样,类中的所有数据处理显示都会作用在界面上。

QOpenGLWidget类有三个重要的虚函数必须实现。

  • void initializeGL();
  • void resizeGL(int w,int h);
  • void paintGL();

initializeGL函数由界面自动调用,并且只调用一次。resizeGL函数会在界面大小调整时自动调用,而paintGL函数绘制界面刷新时自动调用。

渲染代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
void I420Render2::initializeGL()
{
initializeOpenGLFunctions();
const char *vsrc =
"attribute vec4 vertexIn; \
attribute vec4 textureIn; \
varying vec4 textureOut; \
void main(void) \
{ \
gl_Position = vertexIn; \
textureOut = textureIn; \
}";

const char *fsrc =
"varying mediump vec4 textureOut;\n"
"uniform sampler2D textureY;\n"
"uniform sampler2D textureU;\n"
"uniform sampler2D textureV;\n"
"void main(void)\n"
"{\n"
"vec3 yuv; \n"
"vec3 rgb; \n"
"yuv.x = texture2D(textureY, textureOut.st).r; \n"
"yuv.y = texture2D(textureU, textureOut.st).r - 0.5; \n"
"yuv.z = texture2D(textureV, textureOut.st).r - 0.5; \n"
"rgb = mat3( 1, 1, 1, \n"
"0, -0.39465, 2.03211, \n"
"1.13983, -0.58060, 0) * yuv; \n"
"gl_FragColor = vec4(rgb, 1); \n"
"}\n";

m_program.addCacheableShaderFromSourceCode(QOpenGLShader::Vertex,vsrc);
m_program.addCacheableShaderFromSourceCode(QOpenGLShader::Fragment,fsrc);
m_program.link();

GLfloat points[]{
-1.0f, 1.0f,
1.0f, 1.0f,
1.0f, -1.0f,
-1.0f, -1.0f,

0.0f,0.0f,
1.0f,0.0f,
1.0f,1.0f,
0.0f,1.0f
};

vbo.create();
vbo.bind();
vbo.allocate(points,sizeof(points));

GLuint ids[3];
glGenTextures(3,ids);
idY = ids[0];
idU = ids[1];
idV = ids[2];
}

void I420Render2::resizeGL(int w, int h)
{
if(h<=0) h=1;

glViewport(0,0,w,h);
}

void I420Render2::paintGL()
{
if(!ptr) return;

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);

m_program.bind();
vbo.bind();
m_program.enableAttributeArray("vertexIn");
m_program.enableAttributeArray("textureIn");
m_program.setAttributeBuffer("vertexIn",GL_FLOAT, 0, 2, 2*sizeof(GLfloat));
m_program.setAttributeBuffer("textureIn",GL_FLOAT,2 * 4 * sizeof(GLfloat),2,2*sizeof(GLfloat));

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,idY);
glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width,height,0,GL_RED,GL_UNSIGNED_BYTE,ptr);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,idU);
glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width >> 1,height >> 1,0,GL_RED,GL_UNSIGNED_BYTE,ptr + width*height);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D,idV);
glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width >> 1,height >> 1,0,GL_RED,GL_UNSIGNED_BYTE,ptr + width*height*5/4);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

m_program.setUniformValue("textureY",0);
m_program.setUniformValue("textureU",1);
m_program.setUniformValue("textureV",2);
glDrawArrays(GL_QUADS,0,4);
m_program.disableAttributeArray("vertexIn");
m_program.disableAttributeArray("textureIn");
m_program.release();
}

当解码类发送一帧解码完信号时,调用此类的update函数刷新界面。

此种方法在QML中不适用,推荐本工程提供的两种方法都看一下。

第三部分:MainWindow/Widget显示部分

不管是界面是QMainWindow还是QWidget类,要想显示QOpenGLWidget内容就需要界面类中有OpenGL Widget。

ui

黑色部分即为OpenGL Widget组件,在右侧属性栏中可以看到openGLWidget的类为QOpenGLWidget。

点击右键提升为第二部分的渲染类I420Render。

improve

可以看到类由QOpenGLWidget提升为I420Render2(这里有个2是因为有两种写法)。

编译运行,显示界面为:

init

点击Play按钮后会自动打开解码播放显示视频。

play

问题

本文的效果和上一篇相比,亮度明显暗不少。

源码在ffmpeg_beginner10.08.1video_decode_by_cpu_display_by_qopengl09.2video_decode_by_cpu_display_by_qopengl下。可以查看两种写法不同的地方

下一篇:FFmpeg入门教程10.09:软解并使用QML播放视频(YUV420P->OpenGL)


FFmpeg入门教程10.08:软解并使用QOpenGL播放视频(YUV420P)
https://blog.jackeylea.com/ffmpeg/ffmpeg-video-decode-by-cpu-display-by-qopengl-in-yuv420p/
作者
JackeyLea
发布于
2020年5月6日
许可协议