天天看點

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

我在開發過程中Qt用的比較大,是以本系列主要界面由Qt開發。而Qt主要的特性是跨平台,在嵌入式平台中,主要使用QML進行界面開發,如果使用QML開發視訊的話,就需要用到OpenGL了。

本篇主要介紹常用的桌面版的QOpenGL的視訊顯示,桌面版解碼的YUV資料格式為YUV420P,下一篇介紹QML版的視訊顯示,也是YUV420P格式的。

解碼流程圖為:

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

解碼函數調用流程圖為:

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

顯示流程為:

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

解碼顯示流程:

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

共分為三個部分。

第一部分:視訊解碼部分

這個就比較簡單,前面幾篇文章一直在說這個,FFmpeg預設軟解輸出格式為YUV420P。

這裡需要注意的是解碼之後的AVFrame處理。

YUV420P共有三個分量,即:Y、U、V。存儲在三個數組中。

解碼部分代碼為:

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函數繪制界面重新整理時自動調用。

渲染代碼為:

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。

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

黑色部分即為OpenGL Widget元件,在右側屬性欄中可以看到openGLWidget的類為QOpenGLWidget。

點選右鍵提升為第二部分的渲染類I420Render。

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

可以看到類由QOpenGLWidget提升為I420Render2(這裡有個2是因為有兩種寫法)。

編譯運作,顯示界面為:

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

點選

Play

按鈕後會自動打開解碼播放顯示視訊。

FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

問題

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

源碼在Github中

9.video_decode_by_cpu_display_by_qopengl

下。

本文首發于:FFmpeg4入門09:軟解并使用QOpenGL播放視訊(YUV420P)

繼續閱讀