本篇博客目标:读帧解码显示视频
开始进入ffmepg的开发之旅。音视频的细节知识不统一讲解,我在教程中逐点渗透,容我以雷神的话开篇。
视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。
----雷霄骅
对于ffmpeg的架构介绍,请参考24岁“封神”雷霄骅的博客,他已离开江湖,但江湖仍有他的传说。
FFmpeg源代码结构图 - 编码:https://blog.csdn.net/leixiaohua1020/article/details/44226355
FFmpeg源代码结构图 - 解码:https://blog.csdn.net/leixiaohua1020/article/details/44220151
一.ffmpeg开发入门
下面是一个打开视频的小例子。
先用Win32控制台程序来讲解ffmpeg的简单开发,建立Win32的控制台项目,在项目属性中加入ffmpeg的库文件, ffmpeg sdk可以去官网下载,或者用我提供的:ffmpeg3.2.4库文件。
代码如下:
// FFmpeg_打开视频文件.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
extern "C"
{
#include
}
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")
using namespace std;
int main()
{
av_register_all(); //ffmpeg程序的第一句,注册库
AVFormatContext *afc = NULL;
//打开视频文件
int nRet = avformat_open_input(&afc, "天下有情人.mp4", 0, 0);
if (nRet < 0)
{
cout << "找不到视频文件" << endl;
}
else
{
cout << "视频打开成功" << endl;
}
int durTime = afc->duration / AV_TIME_BASE; //视频时间 4分20秒
unsigned int numberOfStream = afc->nb_streams; //包含流的个数2:一个视频流一个音频流
for (int i = 0; i < afc->nb_streams; i++)
{
AVCodecContext *acc = afc->streams[i]->codec;
if (acc->codec_type == AVMEDIA_TYPE_VIDEO) //如果是视频类型
{
AVCodec *codec = avcodec_find_decoder(acc->codec_id);
if (!codec)
{
cout << "没有该类型解码器" << endl;
}
int ret = avcodec_open2(acc, codec, NULL);
if (ret != 0)
{
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf));
}
cout << "解码器打开成功" << endl;
}
}
if (afc)
{
avformat_close_input(&afc); //关闭视频流
}
system("pause");
return 0;
}
可能会出现以下编译错误:
errorC4996: 'AVStream::codec': 被声明为已否决
解决方法如下
由于ffmpeg的源码是C语言写的,在调用它的头文件时,需要用extern"C", 例外导入的lib可以直接放到属性列表,也可以写到代码里。在写ffmpeg程序时, 第一句是av_register_all()用来注册ffmpeg库。
我们是做播放器,需要打开视频文件,avformat_open_input()是打开一个输入流并且读它的头部信息,但编解码器不会被打开,如果打开成功,会返回一个AVFormatContext的实例.该实例包含了很多的视频信息,例如一个视频文件,会有视频流,音频流,字幕流,视频的时间,解码器类型等等信息。视频打开后,需要进行解码,而解码需要解码器,先找解码器avcodec_find_decoder, 找到解码器后再打开解码器,然后进行解码,视频像素转换解析,音频解析,再用线程同步技术实现音视频同步,将视频内容显示在屏幕上。
做视频开发,对于资源的利用要格外重视,打开的资源用完后要及时释放,避免造成过大的内存开销,造成程序的崩溃。
二. 视频播放器FFVideoPlayer的开发
创立Qt GUI项目,工程名称:FFVideoPlayer. 目前的界面如下图,后续根据需求会逐渐优化更新。
中间黑色部分是QOpenGLWidget控件,用来显示视频。
编写各功能模块的代码。
(1)【打开视频】:选择视频文件,打开并显示在OpenGLWidget控件上。实现【打开视频】的槽函数,代码如下:
void FFVideoPlyer::slotOpenFile()
{
QString fname = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("打开视频文件"));
if (fname.isEmpty())
{
return;
}
ui.lineEdit_VideoName->setText(fname);
MyFFmpeg::GetObj()->OpenVideo(fname.toLocal8Bit());
MyFFmpeg::GetObj()->m_isPlay = true;
ui.btn_Play->setText(QString::fromLocal8Bit("暂停"));
}
对于的视频的打开,读帧,解码,像素转换,音频解码等等,这些方法,我封装程类MyFFmpeg. 在项目中添加C++类,类名MyFFmpeg即可。同时为了保证对象的维一性,我们使用单例模式来实现。
本教程的开发流程如下:
打开视频文件,查找解码器,打开解码器的代码如下。为了循序渐进,先实现视频读帧解码,下篇博客进行音频解码。
void MyFFmpeg::OpenVideo(const char *path)
{
mtx.lock();
int nRet = avformat_open_input(&m_afc, path, 0, 0);
for (int i = 0; i < m_afc->nb_streams; i++) //nb_streams打开的视频文件中流的数量,一般nb_streams = 2,音频流和视频流
{
AVCodecContext *acc = m_afc->streams[i]->codec; //分别获取音频流和视频流的解码器
if (acc->codec_type == AVMEDIA_TYPE_VIDEO) //如果是视频
{
m_videoStream = i;
AVCodec *codec = avcodec_find_decoder(acc->codec_id); // 查找解码器
//"没有该类型的解码器"
if (!codec)
{
mtx.unlock();
return;
}
int err = avcodec_open2(acc, codec, NULL); //打开解码器
if (err != 0)
{
//解码器打开失败
}
}
}
mtx.unlock();
}
(2)读帧解码
视频打开后,需要进行读帧,解码,显示,此过程比较耗时,如果放到主线程中,一旦主线程阻塞,就会容易“界面卡死”,所以放到子线线程来实现。添加Qt线程类PlayThread, 继承于QThread,重写线程的run函数。
代码如下:
void PlayThread::run()
{
//在子线程里做什么,当然是读视频帧,解码视频了
//何时读,何时解码呢,在视频打开之后读帧解码, 读帧解码线程要一直运行
//视频没打开之前线程要阻塞, run,while(1)这是基本套路
while (1)
{
if (!(MyFFmpeg::GetObj()->m_isPlay))
{
msleep(5); //调试方便,5微秒后窗口又关闭了,线程继续阻塞,此时可以点击【打开视频按钮】选择视频
continue;
}
while (g_videos.size() > 0)
{
AVPacket pack = g_videos.front();
MyFFmpeg::GetObj()->DecodeFrame(&pack);
av_packet_unref(&pack);
g_videos.pop_front(); //解码完成的帧从list前面弹出
}
AVPacket pkt = MyFFmpeg::GetObj()->ReadFrame();
if (pkt.size <= 0)
{
msleep(10);
}
g_videos.push_back(pkt);
}
}
有些变量的定义,这里不做指出,需要源码的请点击下载。
读帧的实现如下:
AVPacket MyFFmpeg::ReadFrame()
{
AVPacket pkt;
memset(&pkt, 0, sizeof(AVPacket));
mtx.lock();
if (!m_afc)
{
mtx.unlock();
return pkt;
}
int err = av_read_frame(m_afc, &pkt);
if (err != 0)
{
//失败
}
mtx.unlock();
return pkt;
}
解码的实现:
void MyFFmpeg::DecodeFrame(const AVPacket *pkt)
{
mtx.lock();
if (!m_afc)
{
mtx.unlock();
return;
}
if (m_yuv == NULL)
{
m_yuv = av_frame_alloc();
}
AVFrame *frame = m_yuv; //指针传值
int re = avcodec_send_packet(m_afc->streams[pkt->stream_index]->codec, pkt);
if (re != 0)
{
mtx.unlock();
return;
}
re = avcodec_receive_frame(m_afc->streams[pkt->stream_index]->codec, frame);
if (re != 0)
{
//失败
mtx.unlock();
return;
}
mtx.unlock();
}
下面对像素做转换,为显示准备。
bool MyFFmpeg::YuvToRGB(char *out, int outweight, int outheight)
{
mtx.lock();
if (!m_afc || !m_yuv) //像素转换的前提是视频已经打开
{
mtx.unlock();
return false;
}
AVCodecContext *videoCtx = m_afc->streams[this->m_videoStream]->codec;
m_cCtx = sws_getCachedContext(m_cCtx, videoCtx->width, videoCtx->height,
videoCtx->pix_fmt, //像素点的格式
outweight, outheight, //目标宽度与高度
AV_PIX_FMT_BGRA, //输出的格式
SWS_BICUBIC, //算法标记
NULL, NULL, NULL
);
if (m_cCtx)
{
//sws_getCachedContext 成功"
}
else
{
//"sws_getCachedContext 失败"
}
uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };
data[0] = (uint8_t *)out; //指针传值,形参的值会被改变,out的值一直在变,所以QImage每次的画面都不一样,画面就这样显示出来了,这应该是整个开发过程最难的点
int linesize[AV_NUM_DATA_POINTERS] = { 0 };
linesize[0] = outweight * 4; //每一行转码的宽度
//返回转码后的高度
int h = sws_scale(m_cCtx, m_yuv->data, m_yuv->linesize, 0, videoCtx->height,
data,
linesize
);
mtx.unlock();
}
转码处理后的视频是YUV, RGB和色度的四通道, 我们需要把它转化成RGB进行显示。
(3)视频显示
视频的显示用OpenGLWidget显示,把每一帧当做图片来处理,即可显示。关于OpenGLWidget如何显示图片,请查看我给出的方法。下列代码是进行显示,解码后的视频是四通道,所以在给QImage分配空间时用 width() * height() * 4
void VideoViewWidget::paintEvent(QPaintEvent *e)
{
static QImage *image;
if (image == NULL)
{
//视频是YVU四通道的类型。
uchar *buf = new uchar[width() * height() * 4];
image = new QImage(buf, width(), height(), QImage::Format_ARGB32);
}
bool ret = MyFFmpeg::GetObj()->YuvToRGB((char *)(image->bits()), width(), height());
QPainter painter;
painter.begin(this);
painter.drawImage(QPoint(0, 0), *image);
painter.end();
}
当然,在打开界面时,就让子线程运行,但由于视频没有打开,就会一直出阻塞状态,当添加视频文件后,子线程继续运行。画面也就显示了。
效果如下:
只有画面没有音频,而且画面刷新很快,这是由于只解码了视频,没有关音频。下篇进行解码音频。
本篇的源码,请点击【源码下载】。很多ffmpeg的API不懂的,请自行百度深入研究。
tips:
如果我的VS Qt项目你编译不了,请参考以下链接:
https://blog.csdn.net/yao_hou/article/details/84302372
一键解决