目录
最近在学习音视频基础知识,在这里感谢雷神留下的一系列指引新手入门的宝贵资源,虽然他英年早逝,但他的硕果永存。不由感慨真是天妒英才,愿雷神在天堂安好
附上学习资料地址:雷霄骅(leixiaohua1020)的专栏
选择学习ffmpeg的原因是,它具有跨平台特性,Windows、Linux、Android、IOS这些主流系统可以通吃
而且它非常全能,从视频采集、视频编码到视频传输都可以直接使用ffmpeg完成,有雷神留下的学习资料加持,学习起来自然是事半功倍
下面简单记录一下自己使用Qt来做图形界面学习ffmpeg的过程
1.项目配置
首先新建一个Qt Widgets项目
选择一个路径,名字不要带有中文
这里使用的是Qt5.6.2版本32位的工具包
新建一个继承QWidget的类
将在官网获取的ffmpeg库解压到与pro文件同级目录下,这里使用的是ffmpeg-4.2.2版本
获取地址:https://www.ffmpeg.org/download.html
其中包含bin动态库,include头文件,lib引入库
修改工程配置文件VideoPlayerDemo.pro,添加使用ffmpeg依赖的库
新建一个C++类
继承QObject
在新建的类中添加头文件,指示编译器这部分代码按C语言进行编译
先编译输出一下,在同级目录下生成debug路径
打开ffmpeg的bin目录,将其中所有文件(主要是dll动态库)拷贝到debug目录下
项目配置方面小小告一段落
2.显示界面设计
打开界面文件videoplayerdemo.ui,将外层widget设置大小为800*600,并设置最小大小为800*600
拖入两个widget控件,wdg_show——用于显示解码得到的图片、wdg_ctrl——用于添加播放控制按钮,在wdg_ctrl中添加一个pb_play按钮,用于播放视频文件
设置wdg_show、wdg_ctrl的最小大小,设置wdg_ctrl的最大高度,在外层widget上进行垂直布局,可以实现拉伸跟随缩放的效果
新建一个类VideoItem,用于重写paintEvent绘图事件(后面会说为什么要新建这个类)
3.视频解码显示
如果使用主线程进行解码,会造成客户端卡顿的现象,因此这里需要使用多线程开启一个线程去执行解码操作
考虑到需要将解码得到的图片传出,并在Qt控件上显示,因此使用Qt封装的线程函数,便于使用信号和槽机制
- 在videoshow.h中添加头文件<QThread>
- 由这个类继承QThread类
- 更改原有的构造函数,添加Qt线程函数run()
- 添加信号SIG_GetOneImage(const QImage image),这里不使用引用传递的原因是:由于是多线程操作图片,如果传递引用,可能图片在显示处理前已经被释放,所以需要拷贝一份图片
- 添加一会需要打开并进行解码的视频文件
流程描述
- 初始化 ffmpeg,调用av_register_all()才能正常使用编码器和解码器注册所用函数
- 需要分配一个 AVFormatContext,ffmpeg所有的操作都要通过这个 AVFormatContext 来进行,可以理解为视频文件指针
- avformat_open_input()——打开视频文件,avformat_find_stream_info()——获取视频文件信息
- avcodec_find_decoder()——查找解码器,avcodec_open2()——打开解码器
- av_read_frame()——循环读取视频帧
- AVPacket存放的是解码得到的H.264格式的数据,AVFrame存放的是YUV420p格式的数据
- 将解码后的YUV420p 格式视频数据 转换 成 RGB32,抛出信号去控件显示,在Qt控件上绘图显示出来
- 回收数据
VideoShow::VideoShow()
{
m_fileName = "D:/Kugou/华语群星 - 少林英雄.mp4";
}
void VideoShow::run()
{
//1.初始化 FFMPEG 调用了这个才能正常使用编码器和解码器 注册所用函数
av_register_all();
//2.需要分配一个 AVFormatContext,FFMPEG 所有的操作都要通过这个 AVFormatContext 来进行可以理解为视频文件指针
AVFormatContext *pFormatCtx = avformat_alloc_context();
//中文兼容
std::string path = m_fileName.toStdString();
const char* file_path = path.c_str();
//3. 打开视频文件
if( avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0 )
{
qDebug()<<"can't open file";
return;
}
//3.1 获取视频文件信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
{
qDebug()<<"Could't find stream infomation.";
return;
}
//4.读取视频流
int videoStream = -1;
int i;
for (i = 0; i < pFormatCtx->nb_streams; i++)
{
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
}
}
//如果 videoStream 为-1 说明没有找到视频流
if (videoStream == -1)
{
printf("Didn't find a video stream.");
return;
}
//5.查找解码器
auto pCodecCtx = pFormatCtx->streams[videoStream]->codec;
auto pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL)
{
printf("Codec not found.");
return;
}
//打开解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
printf("Could not open codec.");
return;
}
//6.申请解码需要的结构体 AVFrame 视频缓存的结构体
AVFrame *pFrame, *pFrameRGB;
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();
//分配一个 packet
AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));
//7.这里我们将解码后的 YUV 数据转换成 RGB32 YUV420p 格式视频数据-->RGB32--> 图片显示出来
static struct SwsContext *img_convert_ctx;
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width,pCodecCtx->height,
AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
//计算RGB一帧数据大小
auto numBytes = avpicture_get_size(AV_PIX_FMT_RGB32,pCodecCtx->width ,pCodecCtx->height);
//申请RGB一帧画面大小对应的空间
uint8_t * out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
//pFrameRGB与out_buffer绑定
avpicture_fill( (AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_RGB32,
pCodecCtx->width, pCodecCtx->height);
//8.循环读取视频帧, 转换为 RGB 格式, 抛出信号去控件显示
int ret, got_picture;
while(1)
{
//可以看出 av_read_frame 读取的是一帧视频,并存入一个 AVPacket 的结构中
if (av_read_frame(pFormatCtx, packet) < 0)
{
break; //这里认为视频读取完了
}
//生成图片
if (packet->stream_index == videoStream)
{
// 解码 packet(H264) 存在 pFrame(yuv) 里面
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);
if (ret < 0)
{
printf("decode error");
return ;
}
//有解码器解码之后得到的图像数据都是 YUV420 的格式,而这里需要将其保存成图片文件
//因此需要将得到的 YUV420 数据转换成 RGB 格式
if (got_picture)
{
//YUV420转换RGB
sws_scale(img_convert_ctx,
(uint8_t const * const *) pFrame->data,
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
pFrameRGB->linesize);
//out_buffer 与 pFrameRGB 是捆绑的,将 out_buffer 里面的数据存在 QImage 里面
QImage tmpImg((uchar*)out_buffer,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32);
//把图像复制一份 传递给界面显示
QImage image = tmpImg.copy();
//显示到控件 多线程 无法控制控件 所以要发射信号
emit SIG_GetOneImage( image );
}
}
av_free_packet(packet);
msleep(5); // 停一停
}
//9.回收数据
av_free(out_buffer);
av_free(pFrame);
av_free(pFrameRGB);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
}
上述流程解码得到图片image,将解码得到的每一帧图片通过SIG_GetOneImage(image)信号发送出去
给pb_play按钮添加一个处理函数,在VideoPlayer类中实现,点击按钮开启线程,获取图片显示到控件
包含VideoShow类的头文件,添加一个对象,用于连接槽函数
在构造和析构中添加槽函数和回收资源的代码
可以看到接收信号处理的控件是ui->wdg_show,而处理函数slot_setImage并没有定义,这时就需要用到刚才我们新添加的类VideoItem
由于我们不是在最外层的widget上进行绘图,所以需要对控件进行重写paintEvent事件进行绘图(或者使用eventFilter,这里没有用这种方法)
因此我们添加类VideoItem,在其中添加槽函数slot_setImage进行处理,并重写paintEvent事件
槽函数中将image图片保存,并调用repaint重绘
void VideoItem::slot_setImage(const QImage image)
{
m_image = image;
this->repaint();//立即刷新绘图
}
//绘图事件
void VideoItem::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//先画黑色背景
painter.setBrush( Qt::black );
painter.drawRect( 0,0, this->width() , this->height() );
//等比例缩放图片 等比例变成控件这么大
if( m_image.size().width()<= 0 ) return;
QPixmap img = QPixmap::fromImage(m_image.scaled(this->size(), Qt::KeepAspectRatio));
//调整贴图位置,使其居中
//x = (widget_show的宽 - 图片宽) / 2
//y = (widget_show的高 - 图片高) / 2
int x = this->width() - img.width() ;
int y = this->height() - img.height();
x = x/2;
y = y/2;
painter.drawPixmap(x,y,img);
}
还差最后一步,就是将VideoItem类与控件wdg_show关联起来,在videoPlayer.ui中,将wdg_show提升为VideoItem(继承VideoItem)
这样就可以在wdg_show上进行绘图了
4.演示
点击Play按钮,开启线程在VideoShow类中进行解码,得到的图片通过信号SIG_GetOneImage发送出去,在VideoItem中进行重绘,显示在wdg_show控件上