跨平台播放器开发 (五) 如何渲染音视频裸流数据

2,309 阅读5分钟

前言

上一篇咱们学习了 FFmpeg 解码、像素格式转换和音频重采样 ,该篇我们主要学习 QT 跨平台音频视频渲染 API

跨平台播放器开发 (一) QT for MAC OS & FFmpeg 环境搭建

跨平台播放器开发 (二) QT for Linux & FFmpeg 环境搭建

跨平台播放器开发 (三) QT for Windows & FFmpeg 环境搭建

跨平台播放器开发 (四) 开发一个播放器需要用到哪些 FFmpeg 知识

PCM 渲染

其实不管是 Android 的 AudioTrack 亦或者是 OpenSL ES 来渲染 PCM ,原理都是一样的,都是先配置 PCM 的基本信息,比如采样率、通道数量、采样bit数,然后就可以根据声卡的回调来进行 write(pcmBuffer) 数据,我们就根据这个思路步骤,来进行编码。

第一步:设置 PCM 基本信息

配置音频信息我们会使用到 QAudioFormat 对象,根据官网提示,我们要进行多媒体编程,就要配置 multimedia 模块

可以在 CMakeLists.txt 中这样配置

set(QT_VERSION 5)
set(REQUIRED_LIBS Core Gui Widgets Multimedia)
set(REQUIRED_LIBS_QUALIFIED Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Multimedia)
find_package(Qt${QT_VERSION} COMPONENTS ${REQUIRED_LIBS} REQUIRED)
add_executable(qt-audio-debug ${QT_AUDIO_SRC})
target_link_libraries(qt-audio-debug ${REQUIRED_LIBS_QUALIFIED})

下面调用 QAudioFormat 来进行配置音频信息

    QAudioFormat format;
    //设置采样率
    format.setSampleRate(this->sampleRate);
    //设置通道
    format.setChannelCount(this->channelCount);
    //设置采样位数
    format.setSampleSize(this->sampleSize);
    format.setCodec("audio/pcm");
    format.setByteOrder(QAudioFormat::LittleEndian);
    format.setSampleType(QAudioFormat::SignedInt);
    const QAudioDeviceInfo audioDeviceInfo = QAudioDeviceInfo::defaultOutputDevice();
    QAudioDeviceInfo info(audioDeviceInfo);
    //该设置是否支持
    bool audioDeviceOk = info.isFormatSupported(format);
    if (!audioDeviceOk) {
        qWarning() << "Default format not supported - trying to use nearest";
        format = info.nearestFormat(format);
    }

第二步: 将音频数据发送到音频输出设备接口

//将上面配置好的音频数据和设备信息传递给音频输出对象
auto *audioOutput = new QAudioOutput(audioDeviceInfo, format)
//开始播放
audioOutput->start(QIODevice *device)

在播放的时候,需要传入一个 QIODevice 类,这就是咱们前面说的,声卡会给咱们一个回调,用于写入 PCM 数据。如果我们不使用 QIODevice ,而根据死循环一直写入其实是不行的,它底层有一个缓冲区,等缓冲区用完咱们在进行写入数据,这是一个最好的方式。

第三步: 给声卡提供PCM数据

首先我们要进行继承 QIODevice 然后重写 readData 函数

class PCMPlay : public QIODevice {
Q_OBJECT

public:
    PCMPlay();
    ...
    qint64 readData(char *data, qint64 maxlen) override;
    ...
};

#pcmplay.cpp
qint64 PCMPlay::readData(char *data, qint64 maxlen) {
    if (m_pos >= m_buffer.size())
        return 0;
    qint64 total = 0;
    if (!m_buffer.isEmpty()) {
        while (maxlen - total > 0) {
            const qint64 chunk = qMin((m_buffer.size() - m_pos), maxlen - total);
            memcpy(data + total, m_buffer.constData() + m_pos, chunk);
            m_pos = (m_pos + chunk) % m_buffer.size();
            total += chunk;
        }
    }
    return maxlen;
}

上面这一步就相当于我们需要将 pcm 数据 copy 到 readData 的 data 地址中,当底层读取到 data 中的 pcm buf 在送入声卡,那么就会有声音了。

之后如果想暂停或者其它操作,那么可以调用 QAudioOutput 提供的如下函数:

    void stop();
    void reset();
    void suspend();
    void resume();

实现音频播放的代码还是比较少的,这里为了可读性并没有贴出所有代码。 访问完整代码

YUV 渲染

在我的了解中其实在任何设备上都不能直接渲染 YUV 数据,我们只能将 YUV 转为 RGB 格式的数据,才能交于显卡渲染。转换过程上一篇我们使用 ffmpeg 的 sws_getCachedContext sws_scale 该类函数来进行转换,由于使用 ffmpeg 转换太耗内存了,所以咱们这里基于 OpenGL shader 来进行转换,转换公式如下:

const char *fString = GET_STR(
        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.0, 1.0, 1.0,
                       0.0, -0.39465, 2.03211,
                       1.13983, -0.58060, 0.0) * yuv;
            gl_FragColor = vec4(rgb, 1.0);
        }

);

想要在 QT 中使用 OpenGL 需要在 CMakelist.txt 中添加如下代码:

set(QT_VERSION 5)
set(REQUIRED_LIBS Core Gui Widgets Multimedia OpenGL)
set(REQUIRED_LIBS_QUALIFIED Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Multimedia Qt5::OpenGL)
find_package(Qt${QT_VERSION} COMPONENTS ${REQUIRED_LIBS} REQUIRED)
add_executable(qt-audio-debug ${QT_AUDIO_SRC})
target_link_libraries(qt-audio-debug ${REQUIRED_LIBS_QUALIFIED})

根据 QT 中使用 QOpenGLWidget 需要继承它:

class QYUVWidget : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT

public:
    QYUVWidget(QWidget *);

    ~QYUVWidget();
  
    //初始化数据大小
    void InitDrawBufSize(uint64_t size);

    //绘制
    void DrawVideoFrame(unsigned char *data, int frameWidth, int frameHeight);


protected:
    //刷新显示
    void paintGL() override;

    //初始化 gl
    void initializeGL() override;

    //窗口尺寸发生变化
    void resizeGL(int w, int h) override;
...
}

定义 cpp 实现函数:

//用于初始化定义 YUV 大小的 buffer
void QYUVWidget::InitDrawBufSize(uint64_t size) {
    impl->mFrameSize = size;
    impl->mBufYuv = new unsigned char[size];
}

//有新的数据就调用 opengl update 函数,之后会执行 paintGL() 
void QYUVWidget::DrawVideoFrame(unsigned char *data, int frameWidth, int frameHeight) {
    impl->mVideoW = frameWidth;
    impl->mVideoH = frameHeight;
    memcpy(impl->mBufYuv, data, impl->mFrameSize);
    update();
}

//初始化 opengl 函数
void QYUVWidget::initializeGL() {
  //1、初始化 QT Opengl 功能
   initializeOpenGLFunctions();
  
  //2、加载并编译顶点和片元 shader
  impl->mVShader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    //编译顶点 shader program
    if (!impl->mVShader->compileSourceCode(vString)) {
        throw QYUVException();
    }
   impl->mFShader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    //编译片元 shader program
    if (!impl->mFShader->compileSourceCode(fString)) {
        throw QYUVException();
    }
  
  
  //3、创建执行 shader 的程序
    impl->mShaderProgram = new QOpenGLShaderProgram(this);
    //将顶点 片元 shader 添加到程序容器中
    impl->mShaderProgram->addShader(impl->mFShader);
    impl->mShaderProgram->addShader(impl->mVShader);

    
  
  //4、设置顶点片元坐标
    impl->mShaderProgram->bindAttributeLocation("vertexIn", A_VER);

    //设置材质坐标
    impl->mShaderProgram->bindAttributeLocation("textureIn", T_VER);

    //编译shader
    qDebug() << "program.link() = " << impl->mShaderProgram->link();

    qDebug() << "program.bind() = " << impl->mShaderProgram->bind();
  //5、拿到shader 中 纹理y,u,v 的材质
    impl->textureUniformY = impl->mShaderProgram->uniformLocation("tex_y");
    impl->textureUniformU = impl->mShaderProgram->uniformLocation("tex_u");
    impl->textureUniformV = impl->mShaderProgram->uniformLocation("tex_v");

  
  
  //6、加载顶点片元位置
      //顶点
    glVertexAttribPointer(A_VER, 2, GL_FLOAT, 0, 0, VER);
    glEnableVertexAttribArray(A_VER);

    //材质
    glVertexAttribPointer(T_VER, 2, GL_FLOAT, 0, 0, TEX);
    glEnableVertexAttribArray(T_VER);
  
  //7、创建 y,u,v 纹理 id
    glGenTextures(3, texs);
    impl->id_y = texs[0];
    impl->id_u = texs[1];
    impl->id_v = texs[2];
}

//主要讲 y,u,v 数据绑定到对应的纹理 id 上并渲染
void QYUVWidget::paintGL() {
  //1、激活并绑定 y 纹理
   glActiveTexture(GL_TEXTURE0);
   glBindTexture(GL_TEXTURE_2D, impl->id_y);
   glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, impl->mVideoW, impl->mVideoH, 0, GL_LUMINANCE,GL_UNSIGNED_BYTE,
                 impl->mBufYuv);
   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);
  
  //2、激活并绑定 u 纹理
   glActiveTexture(GL_TEXTURE1);//Activate texture unit GL_TEXTURE1
   glBindTexture(GL_TEXTURE_2D, impl->id_u);
   glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, impl->mVideoW / 2, impl->mVideoH / 2, 0, GL_LUMINANCE,
                 GL_UNSIGNED_BYTE, (char *) impl->mBufYuv + impl->mVideoW * impl->mVideoH);
   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);  
  
  //3、激活并绑定 v 纹理
    glActiveTexture(GL_TEXTURE2);//Activate texture unit GL_TEXTURE2
    glBindTexture(GL_TEXTURE_2D, impl->id_v);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, impl->mVideoW / 2, impl->mVideoH / 2, 0, GL_LUMINANCE,
                 GL_UNSIGNED_BYTE, (char *) impl->mBufYuv + impl->mVideoW * impl->mVideoH * 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);
  
  //4、渲染
    //指定y纹理要使用新值,只能用0,1,2等表示纹理单元的索引
    glUniform1i(impl->textureUniformY, 0);
    glUniform1i(impl->textureUniformU, 1);
    glUniform1i(impl->textureUniformV, 2);
  	//渲染
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

//框口改变会进行更新
void QYUVWidget::resizeGL(int w, int h) {
    qDebug() << "resizeGL " << width << ":" << height;
    glViewport(0, 0, w, h);
    update();
}

因为 OpenGL 是跨平台的缘故 ,所以调用接口在任何平台上基本上是一模一样,只要在一个平台学会了,在另一个平台稍微改一下就可以使用。如果对 OpenGL 比较兴趣的可以参考这位大佬总结的 OpenGL ES 3.0 系列使用教程。

程序编译运行,出现如下画面就代表成功了

访问完整代码

总结

利用 QT 跨平台的 API 我们实现了 YUV & PCM 的渲染,总体来说 OpenGL 是不容易上手的,但是只要我们认真的敲几个样例出来,其实也就那么回事儿, 因为使用步骤都差不多。该篇到此结束,下一篇主要写 如何设计一个通用的播放器架构, 敬请期待吧! 再会