跨平臺播放器開發 (五) 如何渲染音視頻裸流數據

前言

上一篇我們學習了 FFmpeg 解碼、像素格式轉換和音頻重採樣 ,該篇咱們主要學習 QT 跨平臺音頻視頻渲染 APIhtml

跨平臺播放器開發 (一) QT for MAC OS & FFmpeg 環境搭建c++

跨平臺播放器開發 (二) QT for Linux & FFmpeg 環境搭建git

跨平臺播放器開發 (三) QT for Windows & FFmpeg 環境搭建github

跨平臺播放器開發 (四) 開發一個播放器須要用到哪些 FFmpeg 知識編程

PCM 渲染

其實不論是 Android 的 AudioTrack 亦或者是 OpenSL ES 來渲染 PCM ,原理都是同樣的,都是先配置 PCM 的基本信息,好比採樣率、通道數量、採樣bit數,而後就能夠根據聲卡的回調來進行 write(pcmBuffer) 數據,咱們就根據這個思路步驟,來進行編碼。markdown

第一步:設置 PCM 基本信息架構

配置音頻信息咱們會使用到 QAudioFormat 對象,根據官網提示,咱們要進行多媒體編程,就要配置 multimedia 模塊ide

能夠在 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 來進行配置音頻信息oop

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() {
  //一、初始化 QT Opengl 功能
   initializeOpenGLFunctions();
  
  //二、加載並編譯頂點和片元 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();
    }
  
  
  //三、建立執行 shader 的程序
    impl->mShaderProgram = new QOpenGLShaderProgram(this);
    //將頂點 片元 shader 添加到程序容器中
    impl->mShaderProgram->addShader(impl->mFShader);
    impl->mShaderProgram->addShader(impl->mVShader);

    
  
  //四、設置頂點片元座標
    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();
  //五、拿到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");

  
  
  //六、加載頂點片元位置
      //頂點
    glVertexAttribPointer(A_VER, 2, GL_FLOAT, 0, 0, VER);
    glEnableVertexAttribArray(A_VER);

    //材質
    glVertexAttribPointer(T_VER, 2, GL_FLOAT, 0, 0, TEX);
    glEnableVertexAttribArray(T_VER);
  
  //七、建立 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() {
  //一、激活並綁定 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);
  
  //二、激活並綁定 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);  
  
  //三、激活並綁定 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);
  
  //四、渲染
    //指定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 是不容易上手的,可是隻要咱們認真的敲幾個樣例出來,其實也就那麼回事兒, 由於使用步驟都差很少。該篇到此結束,下一篇主要寫 如何設計一個通用的播放器架構, 敬請期待吧! 再會

相關文章
相關標籤/搜索