- 视频播放器开发总览
- 最终效果图
- 需要实现的功能
- ffmpeg介绍
- 解封装
- 视频解码
- 音频解码
- seek
- QT播放音频
- opengl显示图像
- opengl如何显示yuv图像
- 滑动条seek
- 屏幕单击双击事件捕获
- 其他注意事项
视频播放器开发总览
大体流程如上图,读取视频文件,解封装将音视频数据分离,然后解码送往设备显示
其中需要用到的技术
qt:做播放器的界面,和用户操作层
ffmpeg:音视频的解封装解码
opengl: 调用显卡将yuv数据转rgb渲染到屏幕
项目地址:
https://gitee.com/lisiwen945/av_codec
最终效果图
需要实现的功能
打开视频文件播放
进度条显示,拖动进度条视频seek功能
视频的播放暂停
单击屏幕暂停播放
双击屏幕全屏和退出全屏
窗体大小变化后,画面和控件需要自适应变化
ffmpeg介绍
FFmpeg有非常强大,几乎涵盖了音视频所有内容,音视频编解码,采集功能、视频格式转换、视频抓图、给视频加水印,直播推流拉流等,很多知名软件如暴风影音 格式工厂都是基于ffmpeg二次开发
我们这次需要用ffmpeg进行解封装和解码,可以去官网下载编译好的库文件,也可以直接用源码编译
官网地址:http://ffmpeg.org/
解封装
// 关键代码解析,详细代码看git链接
// 创建上下文
m_ctx = avformat_alloc_context();
// 打开文件路径
avformat_open_input(&m_ctx, path, 0, &opts);
// 找到视频索引
m_videoIndex = av_find_best_stream(m_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
// 读取一个待解码的包,这个包可能是音频可能是视频
// 通过avPacket->stream_index 来判断是音频还是视频
av_read_frame(m_ctx, avPacket);
视频解码
// 创建视频解码上下文
m_vctx = avcodec_alloc_context3(m_vdec);
avcodec_parameters_to_context(m_vctx, m_videoStream->codecpar);
// 打开解码器
avcodec_open2(m_vctx, m_vdec, nullptr);
// 往解码器灌入数据
avcodec_send_packet(m_vctx, pkt);
// 获取解码结果
avcodec_receive_frame(m_vctx, frame);
注意因为有b帧的存在,会有后向依赖帧,所以并不是输入一帧就能立马有结果,在没有结果的时候解码器会返回EAGAIN,解码结束后会有少量帧残留在解码器,需要输入空数据将其挤出来,即调用avcodec_send_packet(m_actx, nullptr);
音频解码
音频解码与数据解码大致相同
但是音频因为采样率和精度各个文件不相同需要进行重采样
int outsize = av_samples_get_buffer_size(NULL, m_actx->channels,
frame->nb_samples, AV_SAMPLE_FMT_S16, 0);
uint8_t * data[1] = {0};
data[0] = (uint8_t *)malloc(outsize);
int len = swr_convert(m_aswCtx, data, 10000,
(const uint8_t **)frame->data,
frame->nb_samples
);
pcm.data = data[0];
pcm.len = outsize;
seek
int64_t stamp = 0;
stamp = pos * m_ctx->streams[m_videoIndex]->duration;
av_seek_frame(m_ctx, m_videoIndex, stamp,
AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
// seek完要清除缓冲区
avcodec_flush_buffers(m_vctx);
m_pts = m_totalMs * pos;
QT播放音频
QAudioFormat // 详情见qt帮助文档
// 设置声道数量
void setChannelCount(int channels)
// 设置编码格式 "audio/pcm"
void setCodec(const QString &codec)
// 设置采样率
void setSampleRate(int samplerate)
// 设置采样精度 8/16
void setSampleSize(int sampleSize)
// 类型 QAudioFormat::SignedInt UnSignedInt Float
void setSampleType(QAudioFormat::SampleType sampleType)
// 播放行为设置
QAudioOutput
void setVolume(qreal volume)
QIODevice *start()
QAudio::State state() const
void stop()
void suspend()
int bufferSize() const // 缓冲区buf大小
int bytesFree() const // 缓冲区空闲大小
int periodSize() const // 驱动一次处理多少数据
#include <QCoreApplication>
#include <QtMultimedia/QAudioFormat>
#include <QtMultimedia/QAudioOutput>
#include <QThread>
#include <iostream>
#include <direct.h>
// ffmpeg -i video -f s16le test.pcm
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
FILE *fp = fopen("C:/Users/Administrator/Desktop/gitee/av_codec/audio_player/audio_player/test.pcm", "rb");
if (fp == nullptr) {
char buf[1024];
getcwd(buf, 1024);
std::cout << "open file failed pwd " << buf << std::endl;
return 0;
}
QAudioFormat fmt;
fmt.setSampleRate(44100);
fmt.setSampleSize(16);
fmt.setChannelCount(2);
fmt.setCodec("audio/pcm");
fmt.setByteOrder(QAudioFormat::LittleEndian);
fmt.setSampleType(QAudioFormat::UnSignedInt);
QAudioOutput *out = new QAudioOutput(fmt);
QIODevice *io = out->start(); //开始播放
auto minSize = out->periodSize();
auto bufSize = out->bufferSize();
char *buf = new char[bufSize];
while (!feof(fp)) {
int freeSize = out->bytesFree();
if (freeSize < minSize) {
QThread::sleep(0);
continue;
}
auto readSize = fread(buf, 1, freeSize, fp);
io->write(buf, readSize);
}
delete [] buf;
fclose(fp);
return a.exec();
}
opengl显示图像
opengl显示比较复杂,可以跟着https://learnopengl-cn.readthedocs.io/zh/latest/ 教程把顶点着色器,材质贴图,shader程序编写学会
能够在屏幕上画一个三角形,能够将材质贴到指定区域
但是这个官方教程的前置依赖比较多,很多接口比较老旧,而且需要有窗体的支持,建议在qt上opengl开发比较简单
qt上开发opengl可以看b站视频:https://www.bilibili.com/video/BV1UL411W71w?spm_id_from=333.999.0.0
看到第17章如何加载纹理单元就行,这时候你已经掌握了怎么将一个rgb图像显示出来
opengl如何显示yuv图像
与显示rgb图像类似,openg显示yuv图像需要将y,u,v分离为三个材质,将三个材质进行相关转换贴上去,代码如下
// 材质shader程序
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCord;
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main(void)
{
vec3 yuv;
vec3 rgb;
yuv.x = texture2D(tex_y, TexCord).r;
yuv.y = texture2D(tex_u, TexCord).r - 0.5;
yuv.z = texture2D(tex_v, TexCord).r - 0.5;
rgb = mat3(1.0, 1.0, 1.0, 0.0, -0.39465, 2.03211, 1.13983, -0.58060, 0.0) * yuv;
FragColor = vec4(rgb, 1.0);
}
// 顶点shader
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCord;
out vec3 ourColor;
out vec2 TexCord;
void main(){
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);
ourColor=aColor;
TexCord=aTexCord;
}
// 从yuv图像中分离y u v三个材质
auto frame = m_frame->m_avFrame;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texs[0]); //0层绑定到Y材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, frame->data[0]);
//与shader uni遍历关联
glUniform1i(unis[0], 0);
glActiveTexture(GL_TEXTURE0+1);
glBindTexture(GL_TEXTURE_2D, texs[1]); //1层绑定到U材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[1]);
//与shader uni遍历关联
glUniform1i(unis[1],1);
glActiveTexture(GL_TEXTURE0+2);
glBindTexture(GL_TEXTURE_2D, texs[2]); //2层绑定到V材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[2]);
//与shader uni遍历关联
glUniform1i(unis[2], 2);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);
滑动条seek
qt滑动条seek在使用过程中如果点击到某个位置,滑动条并不会立即到这个位置,而是往前前进一小格
这个时候需要重写mousePressEvent方法,实现指哪打哪
void LynSlider::mousePressEvent(QMouseEvent *e)
{
int value = ((float)e->pos().x() / (float)this->width())*(this->maximum() +1);
this->setValue(value);
QSlider::mousePressEvent(e);
}
屏幕单击双击事件捕获
// 单击和双击需要重写下面两个方法
// 单击响应
void MainWindow::mousePressEvent(QMouseEvent *e)
{
if (m_status == STATUS_PAUSE) {
m_status = STATUS_RUNNING;
m_cvStatus.notify_one();
} else {
m_status = STATUS_PAUSE;
}
}
// 双击响应
void MainWindow::mouseDoubleClickEvent(QMouseEvent *e)
{
if (isFullScreen()) {
this->showNormal();
} else {
this->showFullScreen();
}
}
其他注意事项
- 在解码后送显示的过程中不能立即将frame释放,因为更新画布和送帧是异步的,可能还没有来得及显示图像已经被释放了,需要用智能指针引用,在update之后释放
- 在视频播放的时候,滑动条会跟随移动,这个用一个定时器实现,100ms刷新一次,在拖动滑动条的时候,一定要讲定时器取消,在seek完事之后,再重新启动定时器(实测加锁不能解决问题,没搞懂原因)