从零实现简易播放器-2.opengl渲染yuv图像

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shizheng163/article/details/85225029

opengl渲染yuv图像

作者:史正
邮箱:[email protected]
如有错误还请及时指正
如果有错误的描述给您带来不便还请见谅
如需交流请发送邮件,欢迎联系

简述

直接渲染Jpeg等编码后的图像(这里使用的是Jpeg图片)是特别慢的(当张图片>40ms), 常用的做法是直接渲染yuv或者rgb的图像。

这里使用的是使用opengl渲染yuv的图像, 当然也可以通过Qt的绘图库来渲染。不过opengl默认的渲染步骤是在gpu上执行的, 而Qt的绘图库是利于cpu进行计算,gpu进行渲染, 占用cpu的资源比较多。

使用opengl渲染yuv图像的时间单帧图像为(0-2]ms
渲染完成后使主线程等待40-渲染时间, 就会呈现出视频的效果。

这一节的完整代码:

这里给出几个ffmpeg将视频分解为jpeg然后再转换为yuv的命令(由于没有找到视频直接分解为yuv图片帧的办法)

  • ffmpeg -i Suger.mp4 -b 3000k -ss 00:00:10 -t 10 SugerFrames/Frame_%04d.jpeg
  • ffmpeg -y -s 1920x1080 -i SugerFrames/yuv_0001.jpeg SugerYuvs/yuv_0001.yuv

由于没有找到直接将多张jpeg图片直接转换为多张yuv图像的方法, 这里写了一个shell脚本来处理

for((i=1;i<=480;i++))
do
    index=`printf "%04d" $i`
    ffmpeg -y -s 1920x1080 -i SugerFrames/yuv_$index.jpeg SugerYuvs/yuv_$index.yuv
done

ffmpeg 查看yuv图像或视频的方法

  • ffplay.exe -video_size 1920x1080 yuv_0001.yuv

使用QOpenGLWidget进行yuv图片渲染

除了可以选择使用QPainter和标准的OpenGL渲染图形,QOpenGLWidget类提供了在Qt应用程序中显示OpenGL图形的功能。

关于QOpenGLWidget的介绍可以参考以下文章:

笔者对opengl基本上没什么了解, 渲染部分选自以下博客,感谢博主

另外glviewport是opengl渲染的图片显示的位置, 坐标原点为左下角。

使用了opengl渲染之后,再使用QPainter渲染(当时只是想使用这个渲染默认的Jpeg图标)就会导致opengl和QPainter渲染的效果无法使用。

解决方法还没有找到, 所以干脆就把默认图片也转成了yuv图像。

.h文件

#ifndef VIDEOGLWIDGET_H
#define VIDEOGLWIDGET_H
#include <QWidget>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <memory>
#include <mutex>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>
#include <QOpenGLTexture>
#include "fileutil.h"
class VideoGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    QOBJECT_H
public:
    VideoGLWidget(QWidget * pParent = 0);
    virtual                 ~VideoGLWidget();
    void                    PictureShow(fileutil::PictureFilePtr pPicture);
    void                    DefaultPictureShow();

protected:
    void                    initializeGL();
    void                    resizeGL(int w, int h);
    void                    paintGL();

private:
    //绘图是异步动作, 防止绘图过程中,图片被置为NULL,所以需要加锁
    std::mutex                  m_mutexForShowYuvData;
    bool                        m_bIsShowVideoIcon; //是否要显示视频默认图标
    fileutil::PictureFilePtr    m_pDefaultPict;//默认图片
    fileutil::PictureFilePtr    m_pYuvPictPtr;//当前需要显示的Yuv数据

    //显示Opengl渲染后图像位置
    QRect                       m_drawRect;

    //渲染yuv图像变量
    GLuint                      textureUniformY; //y纹理数据位置
    GLuint                      textureUniformU; //u纹理数据位置
    GLuint                      textureUniformV; //v纹理数据位置
    GLuint                      id_y; //y纹理对象ID
    GLuint                      id_u; //u纹理对象ID
    GLuint                      id_v; //v纹理对象ID
    QOpenGLTexture*             m_pTextureY;  //y纹理对象
    QOpenGLTexture*             m_pTextureU;  //u纹理对象
    QOpenGLTexture*             m_pTextureV;  //v纹理对象
    QOpenGLShader *             m_pVSHader;  //顶点着色器程序对象
    QOpenGLShader *             m_pFSHader;  //片段着色器对象
    QOpenGLShaderProgram        *m_pShaderProgram; //着色器程序容器
};

#endif // VIDEOGLWIDGET_H

PicturePtr结构如下

#include <string>
#include <stdint.h>
#include <memory>
struct FileRawData
{
    FileRawData(){
        m_pData = NULL;
    }
    FileRawData(FileRawData && filedata)
    {
        m_pData = filedata.m_pData;
        m_uLen = filedata.m_uLen;
        m_filename = filedata.m_filename;
        filedata.m_pData = NULL;
        filedata.m_uLen = 0;
        filedata.m_filename.clear();
    }
    ~FileRawData()
    {
        if(m_pData)
            delete m_pData;
        m_pData = NULL;
        m_uLen = 0;
    }
    uint8_t *   m_pData;
    uint32_t    m_uLen;
    std::string m_filename;
};
typedef std::shared_ptr<FileRawData> FileRawDataPtr;
struct PictureFile: public FileRawData
{
    enum PictureFormat
    {
        kFormatYuv,
        kFormatJpeg
    };
    PictureFile(FileRawData & parent, uint32_t nWeight, uint32_t nHeight, PictureFormat pictFormat)
        :FileRawData(std::move(parent))
        ,m_nWeight(nWeight)
        ,m_nHeight(nHeight)
        ,m_pictFormat(pictFormat)
    {

    }
    uint32_t        m_nWeight;
    uint32_t        m_nHeight;
    PictureFormat   m_pictFormat;
};
typedef std::shared_ptr<PictureFile> PictureFilePtr;

cpp文件

#include "videoglwidget.h"
#include <QPainter>
#include <QDebug>
#include <ctime>
#include <QOpenGLTexture>
#include <QOpenGLBuffer>
#include "logutil.h"
//opengl 渲染部分参考自Csdn博客:https://blog.csdn.net/su_vast/article/details/52214642
using namespace fileutil;
#define DEFAULT_ICO_VIEW_WIDTH 48
#define DEFAULT_ICO_VIEW_HEIGHT 48
#define DEFAULT_ICO_PIX_HEIGHT 512
#define DEFAULT_ICO_PIX_WIDTH 512
VideoGLWidget::VideoGLWidget(QWidget *pParent)
    :QOpenGLWidget(pParent)
{
    textureUniformY = 0;
    textureUniformU = 0;
    textureUniformV = 0;
    id_y = 0;
    id_u = 0;
    id_v = 0;
    m_pYuvPictPtr = NULL;
    m_pVSHader = NULL;
    m_pFSHader = NULL;
    m_pShaderProgram = NULL;
    m_pTextureY = NULL;
    m_pTextureU = NULL;
    m_pTextureV = NULL;
    //默认显示渲染位置为默认视频图标位置
    m_drawRect = QRect((width() - DEFAULT_ICO_VIEW_WIDTH)/2, height()/2, DEFAULT_ICO_VIEW_WIDTH, DEFAULT_ICO_VIEW_HEIGHT);
    this->DefaultPictureShow();
}

VideoGLWidget::~VideoGLWidget()
{
    if(m_pVSHader)
    {
        delete m_pVSHader;
        m_pVSHader = NULL;
    }
    if(m_pFSHader)
    {
        delete m_pFSHader;
        m_pFSHader = NULL;
    }
    if(m_pShaderProgram)
    {
        delete m_pShaderProgram;
        m_pShaderProgram = NULL;
    }
    if(m_pTextureY)
    {
        delete m_pTextureY;
        m_pTextureY = NULL;
    }
    if(m_pTextureU)
    {
        delete m_pTextureU;
        m_pTextureU = NULL;
    }
    if(m_pTextureV)
    {
        delete m_pTextureV;
        m_pTextureV = NULL;
    }
}

void VideoGLWidget::PictureShow(PictureFilePtr pPicture)
{
    std::unique_lock<std::mutex> locker(m_mutexForShowYuvData);
    m_pYuvPictPtr = pPicture;
    m_drawRect = QRect(0, 0, width(), height());
    this->update();
}

void VideoGLWidget::DefaultPictureShow()
{
    std::unique_lock<std::mutex> locker(m_mutexForShowYuvData);
    if(!m_pDefaultPict)
    {
        FileRawDataPtr pFileData = fileutil::ReadFileRawData("../../images/video.yuv");
        m_pDefaultPict = PictureFilePtr(new PictureFile(*pFileData.get(), DEFAULT_ICO_PIX_WIDTH, DEFAULT_ICO_PIX_HEIGHT, PictureFile::kFormatYuv));
    }
    m_pYuvPictPtr = m_pDefaultPict;
    m_drawRect = QRect((width() - DEFAULT_ICO_VIEW_WIDTH)/2, (height() - DEFAULT_ICO_VIEW_HEIGHT) /2, DEFAULT_ICO_VIEW_WIDTH, DEFAULT_ICO_VIEW_HEIGHT);
    this->update();
}

void VideoGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glEnable(GL_DEPTH_TEST);
    //现代opengl渲染管线依赖着色器来处理传入的数据
    //着色器:就是使用openGL着色语言(OpenGL Shading Language, GLSL)编写的一个小函数,
    //GLSL是构成所有OpenGL着色器的语言,具体的GLSL语言的语法需要读者查找相关资料
    //初始化顶点着色器 对象
    m_pVSHader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc ="\
            attribute vec4 vertexIn; \
    attribute vec2 textureIn; \
    varying vec2 textureOut;  \
    void main(void)           \
    {                         \
        gl_Position = vertexIn; \
        textureOut = textureIn; \
    }";
    m_pVSHader->compileSourceCode(vsrc);
    //初始化片段着色器 功能gpu中yuv转换成rgb
    m_pFSHader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    //片段着色器源码
    const char *fsrc = "varying vec2 textureOut; \
            uniform sampler2D tex_y; \
    uniform sampler2D tex_u; \
    uniform sampler2D tex_v; \
    void main(void) \
    { \
        vec3 yuv; \
        vec3 rgb; \
        yuv.x = texture2D(tex_y, textureOut).r; \
        yuv.y = texture2D(tex_u, textureOut).r - 0.5; \
        yuv.z = texture2D(tex_v, textureOut).r - 0.5; \
        rgb = mat3( 1,       1,         1, \
                    0,       -0.39465,  2.03211, \
                    1.13983, -0.58060,  0) * yuv; \
        gl_FragColor = vec4(rgb, 1); \
    }";
    m_pFSHader->compileSourceCode(fsrc);
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
#define ATTRIB_VERTEX 3
#define ATTRIB_TEXTURE 4
    //创建着色器程序容器
    m_pShaderProgram = new QOpenGLShaderProgram;
    //将片段着色器添加到程序容器
    m_pShaderProgram->addShader(m_pFSHader);
    //将顶点着色器添加到程序容器
    m_pShaderProgram->addShader(m_pVSHader);
    //绑定属性vertexIn到指定位置ATTRIB_VERTEX,该属性在顶点着色源码其中有声明
    m_pShaderProgram->bindAttributeLocation("vertexIn", ATTRIB_VERTEX);
    //绑定属性textureIn到指定位置ATTRIB_TEXTURE,该属性在顶点着色源码其中有声明
    m_pShaderProgram->bindAttributeLocation("textureIn", ATTRIB_TEXTURE);
    //链接所有所有添入到的着色器程序
    m_pShaderProgram->link();
    //激活所有链接
    m_pShaderProgram->bind();
    //读取着色器中的数据变量tex_y, tex_u, tex_v的位置,这些变量的声明可以在
    //片段着色器源码中可以看到
    textureUniformY = m_pShaderProgram->uniformLocation("tex_y");
    textureUniformU =  m_pShaderProgram->uniformLocation("tex_u");
    textureUniformV =  m_pShaderProgram->uniformLocation("tex_v");
    // 顶点矩阵
    static const GLfloat vertexVertices[] = {
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f, 1.0f,
        1.0f, 1.0f,
    };
    //纹理矩阵
    static const GLfloat textureVertices[] = {
        0.0f,  1.0f,
        1.0f,  1.0f,
        0.0f,  0.0f,
        1.0f,  0.0f,
    };
    //设置属性ATTRIB_VERTEX的顶点矩阵值以及格式
    glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, vertexVertices);
    //设置属性ATTRIB_TEXTURE的纹理矩阵值以及格式
    glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, textureVertices);
    //启用ATTRIB_VERTEX属性的数据,默认是关闭的
    glEnableVertexAttribArray(ATTRIB_VERTEX);
    //启用ATTRIB_TEXTURE属性的数据,默认是关闭的
    glEnableVertexAttribArray(ATTRIB_TEXTURE);
    //分别创建y,u,v纹理对象
    m_pTextureY = new QOpenGLTexture(QOpenGLTexture::Target2D);
    m_pTextureU = new QOpenGLTexture(QOpenGLTexture::Target2D);
    m_pTextureV = new QOpenGLTexture(QOpenGLTexture::Target2D);
    m_pTextureY->create();
    m_pTextureU->create();
    m_pTextureV->create();
    //获取返回y分量的纹理索引值
    id_y = m_pTextureY->textureId();
    //获取返回u分量的纹理索引值
    id_u = m_pTextureU->textureId();
    //获取返回v分量的纹理索引值
    id_v = m_pTextureV->textureId();
    //    glClearColor(0.3,0.3,0.3,0.0);//设置背景色
}

void VideoGLWidget::resizeGL(int w, int h)
{
    if(h == 0)// 防止被零除
    {
        h = 1;// 将高设为1
    }
    glViewport(0, 0, w, h);
}

void VideoGLWidget::paintGL()
{
    std::unique_lock<std::mutex> locker(m_mutexForShowYuvData);
    if(m_pYuvPictPtr)
    {
        //        clock_t start = clock();
        uint32_t weight = m_pYuvPictPtr->m_nWeight;
        uint32_t height = m_pYuvPictPtr->m_nHeight;
        glViewport(m_drawRect.x(), m_drawRect.y(), m_drawRect.width(), m_drawRect.height());
        //加载y数据纹理
        //激活纹理单元GL_TEXTURE0
        glActiveTexture(GL_TEXTURE0);
        //使用来自y数据生成纹理
        glBindTexture(GL_TEXTURE_2D, id_y);
        //使用内存中m_pBufYuv420p数据创建真正的y数据纹理
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, weight, height, 0, GL_RED, GL_UNSIGNED_BYTE, m_pYuvPictPtr->m_pData);
        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);
        //加载u数据纹理
        glActiveTexture(GL_TEXTURE1);//激活纹理单元GL_TEXTURE1
        glBindTexture(GL_TEXTURE_2D, id_u);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, weight/2, height/2, 0, GL_RED, GL_UNSIGNED_BYTE, (char*)m_pYuvPictPtr->m_pData+weight*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);

        //加载v数据纹理
        glActiveTexture(GL_TEXTURE2);//激活纹理单元GL_TEXTURE2
        glBindTexture(GL_TEXTURE_2D, id_v);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, weight/2, height/2, 0, GL_RED, GL_UNSIGNED_BYTE, (char*)m_pYuvPictPtr->m_pData+weight*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);
        //指定y纹理要使用新值 只能用0,1,2等表示纹理单元的索引,这是opengl不人性化的地方
        //0对应纹理单元GL_TEXTURE0 1对应纹理单元GL_TEXTURE1 2对应纹理的单元
        glUniform1i(textureUniformY, 0);
        //指定u纹理要使用新值
        glUniform1i(textureUniformU, 1);
        //指定v纹理要使用新值
        glUniform1i(textureUniformV, 2);
        //使用顶点数组方式绘制图形
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        //        clock_t end = clock();
    }
}

猜你喜欢

转载自blog.csdn.net/shizheng163/article/details/85225029