跨平台播放器开发 (五) 如何渲染音视频裸流数据
前言
上一篇咱们学习了 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();
实现音频播放的代码还是比较少的,这里为了可读性并没有贴出所有代码。
访问完整代码:https://github.com/yangkun19921001/YKAVStudyPlatform/blob/main/avcore/qt/audio/main.cpp
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: https://github.com/githubhaohao/NDK_OpenGLES_3_0 系列使用教程。
程序编译运行,出现如下画面就代表成功了
访问完整代码:https://github.com/yangkun19921001/YKAVStudyPlatform/blob/main/avcore/qt/video/main.cpp
总结
利用 QT 跨平台的 API 我们实现了 YUV & PCM 的渲染,总体来说 「OpenGL」 是不容易上手的,但是只要我们认真的敲几个样例出来,其实也就那么回事儿, 因为使用步骤都差不多。该篇到此结束,下一篇主要写 「如何设计一个通用的播放器架构」, 敬请期待吧! 再会