跨平臺播放器開發(fā) (五) 如何渲染音視頻裸流數(shù)據(jù)
前言
上一篇咱們學(xué)習(xí)了 FFmpeg 解碼、像素格式轉(zhuǎn)換和音頻重采樣 ,該篇我們主要學(xué)習(xí) QT 跨平臺音頻視頻渲染 API 。
?跨平臺播放器開發(fā) (一) QT for MAC OS & FFmpeg 環(huán)境搭建
跨平臺播放器開發(fā) (二) QT for Linux & FFmpeg 環(huán)境搭建
跨平臺播放器開發(fā) (三) QT for Windows & FFmpeg 環(huán)境搭建
跨平臺播放器開發(fā) (四) 開發(fā)一個播放器需要用到哪些 FFmpeg 知識
?
PCM 渲染
其實不管是 Android 的 AudioTrack 亦或者是 OpenSL ES 來渲染 PCM ,原理都是一樣的,都是先配置 PCM 的基本信息,比如采樣率、通道數(shù)量、采樣bit數(shù),然后就可以根據(jù)聲卡的回調(diào)來進(jìn)行 write(pcmBuffer) 數(shù)據(jù),我們就根據(jù)這個思路步驟,來進(jìn)行編碼。
「第一步:設(shè)置 PCM 基本信息」
配置音頻信息我們會使用到 QAudioFormat 對象,根據(jù)官網(wǎng)提示,我們要進(jìn)行多媒體編程,就要配置 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})
下面調(diào)用 QAudioFormat 來進(jìn)行配置音頻信息
QAudioFormat format;
//設(shè)置采樣率
format.setSampleRate(this->sampleRate);
//設(shè)置通道
format.setChannelCount(this->channelCount);
//設(shè)置采樣位數(shù)
format.setSampleSize(this->sampleSize);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
const QAudioDeviceInfo audioDeviceInfo = QAudioDeviceInfo::defaultOutputDevice();
QAudioDeviceInfo info(audioDeviceInfo);
//該設(shè)置是否支持
bool audioDeviceOk = info.isFormatSupported(format);
if (!audioDeviceOk) {
qWarning() << "Default format not supported - trying to use nearest";
format = info.nearestFormat(format);
}
「第二步: 將音頻數(shù)據(jù)發(fā)送到音頻輸出設(shè)備接口」
//將上面配置好的音頻數(shù)據(jù)和設(shè)備信息傳遞給音頻輸出對象
auto *audioOutput = new QAudioOutput(audioDeviceInfo, format)
//開始播放
audioOutput->start(QIODevice *device)
在播放的時候,需要傳入一個 QIODevice 類,這就是咱們前面說的,聲卡會給咱們一個回調(diào),用于寫入 PCM 數(shù)據(jù)。如果我們不使用 QIODevice ,而根據(jù)死循環(huán)一直寫入其實是不行的,它底層有一個緩沖區(qū),等緩沖區(qū)用完咱們在進(jìn)行寫入數(shù)據(jù),這是一個最好的方式。
「第三步: 給聲卡提供PCM數(shù)據(jù)」
首先我們要進(jìn)行繼承 QIODevice 然后重寫 readData 函數(shù)
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;
}
上面這一步就相當(dāng)于我們需要將 pcm 數(shù)據(jù) copy 到 readData 的 data 地址中,當(dāng)?shù)讓幼x取到 data 中的 pcm buf 在送入聲卡,那么就會有聲音了。
之后如果想暫?;蛘咂渌僮鳎敲纯梢哉{(diào)用 QAudioOutput 提供的如下函數(shù):
void stop();
void reset();
void suspend();
void resume();
實現(xiàn)音頻播放的代碼還是比較少的,這里為了可讀性并沒有貼出所有代碼。
訪問完整代碼:https://github.com/yangkun19921001/YKAVStudyPlatform/blob/main/avcore/qt/audio/main.cpp
YUV 渲染
在我的了解中其實在任何設(shè)備上都不能直接渲染 YUV 數(shù)據(jù),我們只能將 YUV 轉(zhuǎn)為 RGB 格式的數(shù)據(jù),才能交于顯卡渲染。轉(zhuǎn)換過程上一篇我們使用 ffmpeg 的 sws_getCachedContext sws_scale 該類函數(shù)來進(jìn)行轉(zhuǎn)換,由于使用 ffmpeg 轉(zhuǎn)換太耗內(nèi)存了,所以咱們這里基于 OpenGL shader 來進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換公式如下:

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})
根據(jù) QT 中使用 QOpenGLWidget 需要繼承它:
class QYUVWidget : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT
public:
QYUVWidget(QWidget *);
~QYUVWidget();
//初始化數(shù)據(jù)大小
void InitDrawBufSize(uint64_t size);
//繪制
void DrawVideoFrame(unsigned char *data, int frameWidth, int frameHeight);
protected:
//刷新顯示
void paintGL() override;
//初始化 gl
void initializeGL() override;
//窗口尺寸發(fā)生變化
void resizeGL(int w, int h) override;
...
}
定義 cpp 實現(xiàn)函數(shù):
//用于初始化定義 YUV 大小的 buffer
void QYUVWidget::InitDrawBufSize(uint64_t size) {
impl->mFrameSize = size;
impl->mBufYuv = new unsigned char[size];
}
//有新的數(shù)據(jù)就調(diào)用 opengl update 函數(shù),之后會執(zhí)行 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 函數(shù)
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、創(chuàng)建執(zhí)行 shader 的程序
impl->mShaderProgram = new QOpenGLShaderProgram(this);
//將頂點 片元 shader 添加到程序容器中
impl->mShaderProgram->addShader(impl->mFShader);
impl->mShaderProgram->addShader(impl->mVShader);
//4、設(shè)置頂點片元坐標(biāo)
impl->mShaderProgram->bindAttributeLocation("vertexIn", A_VER);
//設(shè)置材質(zhì)坐標(biāo)
impl->mShaderProgram->bindAttributeLocation("textureIn", T_VER);
//編譯shader
qDebug() << "program.link() = " << impl->mShaderProgram->link();
qDebug() << "program.bind() = " << impl->mShaderProgram->bind();
//5、拿到shader 中 紋理y,u,v 的材質(zhì)
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);
//材質(zhì)
glVertexAttribPointer(T_VER, 2, GL_FLOAT, 0, 0, TEX);
glEnableVertexAttribArray(T_VER);
//7、創(chuàng)建 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 數(shù)據(jù)綁定到對應(yīng)的紋理 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);
}
//框口改變會進(jìn)行更新
void QYUVWidget::resizeGL(int w, int h) {
qDebug() << "resizeGL " << width << ":" << height;
glViewport(0, 0, w, h);
update();
}
因為 「OpenGL 是跨平臺的緣故」 ,所以調(diào)用接口在任何平臺上基本上是一模一樣,只要在一個平臺學(xué)會了,在另一個平臺稍微改一下就可以使用。如果對 OpenGL 比較興趣的可以參考這位大佬總結(jié)的 OpenGL ES 3.0: https://github.com/githubhaohao/NDK_OpenGLES_3_0 系列使用教程。
程序編譯運行,出現(xiàn)如下畫面就代表成功了

訪問完整代碼:https://github.com/yangkun19921001/YKAVStudyPlatform/blob/main/avcore/qt/video/main.cpp
總結(jié)
利用 QT 跨平臺的 API 我們實現(xiàn)了 YUV & PCM 的渲染,總體來說 「OpenGL」 是不容易上手的,但是只要我們認(rèn)真的敲幾個樣例出來,其實也就那么回事兒, 因為使用步驟都差不多。該篇到此結(jié)束,下一篇主要寫 「如何設(shè)計一個通用的播放器架構(gòu)」, 敬請期待吧! 再會
