上一篇我們學習了 FFmpeg 解碼、像素格式轉換和音頻重採樣 ,該篇咱們主要學習 QT 跨平臺音頻視頻渲染 API
。html
跨平臺播放器開發 (一) QT for MAC OS & FFmpeg 環境搭建c++
跨平臺播放器開發 (二) QT for Linux & FFmpeg 環境搭建git
其實不論是 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
轉爲 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 是不容易上手的,可是隻要咱們認真的敲幾個樣例出來,其實也就那麼回事兒, 由於使用步驟都差很少。該篇到此結束,下一篇主要寫 如何設計一個通用的播放器架構, 敬請期待吧! 再會