前面我们已经完成了工程的构建、ffmpeg装载以及基本控件添加,现在就开始BPlay1.0核心部分的分析:视频播放。
1、解封装
Bffmpeg::BLoadMediaFile这个方法在 BPlay1.0系列(4:Bffmpeg单例模式设计+媒体流文件检测) 提到过,当时说是用于检测媒体文件的合法性而构建的,其实其本质就是解封装媒体文件,通过解封装来判断文件是否可以成功载入。在讲解解封装之前,需要先了解这一步在整个视频播放流程的作用和地位,以及播放器在运行的时候究竟需要做哪些事。
上图描述了一个封装数据格式文件在播放时的完整步骤,可以看到第一步就是解封装,以MP4文件为例,它是由编码视频+编码音频合成的,解封装(Bffmpeg::BLoadMediaFile)就是将MP4拆成两部分:编码视频和编码音频,并保存相关信息到FormatContext中,同时获取到音/视频对应的索引和解码器,用于后续音视频解码。
2、音视频解码
首先在音视频主控线程里面,需要不断获取帧数据,将视频帧和音频帧分别加到各自的队列里面:
/********************************
* void Bffmpeg::run()
* 功能:音视频主控线程
* *****************************/
void Bffmpeg::run()
{
int Ret = 0;
AVPacket pkt;
int c = 0;
while (ffmpegrun) {
if (AudioQue.que.size() > 5 && VideoQue.que.size() > 5) {
msleep(10);
continue;
}
Ret = av_read_frame(FormatContext, &pkt);
if (0 != Ret) {
BLOG("av_read_frame fail, Ret[%d]", Ret);
return;
}
if (pkt.stream_index == Audio_index) {
/* 音频流入队列 */
QMutexLocker locker(&AudioQue.mtx);
AudioQue.que.push_back(pkt);
} else if (pkt.stream_index == Video_index) {
/* 视频流入队列 */
QMutexLocker locker(&VideoQue.mtx);
VideoQue.que.push_back(pkt);
}
msleep(1);
}
}
接着时视频解码线程Bvideo::run,它不断从VideoQue队列里面取数据(编码),解码后送到frameque(RGB队列):
/********************************
* void Bvideo::run()
* 功能:视频解码线程
* *****************************/
void Bvideo::run()
{
while (videorun)
{
AVPacket pkt;
if (Bffmpeg::GetInstance()->GetVideoQue().que.size() == 0) {
msleep(10);
continue;
} else {
/* 视频流编码数据出队列 */
QMutexLocker Locker(&Bffmpeg::GetInstance()->GetVideoQue().mtx);
AVPacket pkt = Bffmpeg::GetInstance()->GetVideoQue().que.front();
Bffmpeg::GetInstance()->GetVideoQue().que.pop_front();
}
/* 解码 */
AVFrame* frame = Bffmpeg::GetInstance()->Decode(pkt);
av_packet_unref(&pkt);
if (frame == NULL) {
continue;
}
/* 解码帧数据入RGB队列 */
QMutexLocker FrameLocker(&frameque.mtx);
frameque.frame.push_back(frame);
msleep(1);
}
return;
}
视频解码线程了负责解码的方法实际是Bffmpeg::GetInstance()->Decode(pkt):
/********************************
* AVFrame* Bffmpeg::Decode(AVPacket &pkt)
* 功能:音视频解码
* *****************************/
AVFrame* Bffmpeg::Decode(AVPacket &pkt)
{
int Ret = 0;
AVFrame *frame = av_frame_alloc(); /* 存在反复申请释放(转RGB后释放),后续优化 */
/* 发送数据到ffmepg,放到解码队列中 */
Ret = avcodec_send_packet(FormatContext->streams[pkt.stream_index]->codec, &pkt);
if (0 != Ret) {
BLOG("avcodec_send_packet fail, ret:%d", Ret);
return NULL;
}
/* 从解码队列中取出frame */
Ret = avcodec_receive_frame(FormatContext->streams[pkt.stream_index]->codec, frame);
if (0 != Ret) {
BLOG("avcodec_send_paavcodec_receive_framecket fail, ret:%d", Ret);
return NULL;
}
return frame;
}
经过这几个关键接口,视频解码数据已经生成,并且通过frameque(RGB队列)转到显示模块(Bwidget)了。
3、视频显示
显示视频帧主要是通过Bwidget的paintEvent实现,通过定时器(update方法)触发画图事件,先从frame队列取出解码数据,再进行格式统一转化(RGBA32),最后就可以在画板上呈现出来了:
/********************************
* void Bwidget::paintEvent(QPaintEvent *event)
* 功能:绘制一帧图像
* *****************************/
void Bwidget::paintEvent(QPaintEvent *event)
{
if (Bvideo::GetInstance()->GetFrameque().frame.size() == 0) {
return;
}
/* 解码数据出队列 */
QMutexLocker Locker(&Bvideo::GetInstance()->GetFrameque().mtx);
AVFrame* frame = Bvideo::GetInstance()->GetFrameque().frame.front();
SwsContext *context = NULL;
AVCodecContext *codec = Bffmpeg::GetInstance()->GetFormatContext()->streams[Bffmpeg::GetInstance()->GetVideoIndex()]->codec;
/* 解码数据转RGB32 */
context = sws_getCachedContext(context, /* 转化上下文结构体 */
codec->width, codec->height, codec->pix_fmt, /* 源图像格式和宽高 */
Width, Height, AV_PIX_FMT_BGRA, /* 目标图像格式和宽高 */
SWS_BICUBIC, NULL, NULL, NULL);
if (NULL == context) {
BLOG("sws_getCachedContext fail");
return;
}
uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };
data[0] = (uint8_t *)Image->bits();
int linesize[AV_NUM_DATA_POINTERS] = { 0 };
linesize[0] = Width * 4; /* 实际显示一行的宽度,32位4个字节 */
sws_scale(context, frame->data, /* 源视频解码数据 */
frame->linesize, /* 每行大小 */
0, /* 用不到 */
frame->height, /* 图像高度 */
data, /* 输出的每个通道数据指针 */
linesize); /* 每个通道行字节数 */
/* 释放解码数据,出队列 */
av_frame_free(&frame);
Bvideo::GetInstance()->GetFrameque().frame.pop_front();
/* 绘制图像 */
QPainter painter;
painter.begin(this);
painter.drawImage(QPoint(X, Y), *Image);
painter.end();
return;
}
此外,对于这个画板还有两个初始化的关键方法Bwidget::InitMedia(),在加载完媒体文件后调用,主要是相关信息获取、内存申请,画布规划这些:
/********************************
* void Bwidget::InitMedia()
* 功能:初始化媒体画布相关信息
* *****************************/
void Bwidget::InitMedia()
{
int WidgetWidth = width(); /* Bwidget宽 */
int WidgetHeight = height(); /* Bwidget高 */
int VideoIndex = Bffmpeg::GetInstance()->GetVideoIndex();
int MediaWidth = Bffmpeg::GetInstance()->GetFormatContext()->streams[VideoIndex]->codec->width; /* 视频宽 */
int MediaHeight = Bffmpeg::GetInstance()->GetFormatContext()->streams[VideoIndex]->codec->height; /* 视频高 */
if (((float)WidgetWidth / (float)WidgetHeight) > ((float)MediaWidth / (float)MediaHeight)) {
/* Bwidget宽高比大于视频,左右需要有黑边 */
Width = (int)((float)WidgetHeight * (float)MediaWidth / (float)MediaHeight);
Height = WidgetHeight;
X = (WidgetWidth - Width) / 2;
Y = 0;
} else {
/* Bwidget宽高比小于视频,上下需要有黑边 */
Width = WidgetWidth;
Height = ((float)WidgetWidth * (float)MediaHeight / (float)MediaWidth);
X = 0;
Y = (WidgetHeight - Height) / 2;;
}
if (ImageData) {
delete ImageData;
}
if (Image) {
delete Image;
}
/* 初始化视频区域 */
ImageData = new uchar[Width * Height * 4];
Image = new QImage(ImageData, Width, Height, QImage::Format_RGB32);
return;
}
最后构建工程,效果如下: