本篇博客目标:读帧解码显示视频

        开始进入ffmepg的开发之旅。音视频的细节知识不统一讲解,我在教程中逐点渗透,容我以雷神的话开篇。

     
 视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。

                                                                             
                                       ----雷霄骅

 

       对于ffmpeg的架构介绍,请参考24岁“封神”雷霄骅的博客,他已离开江湖,但江湖仍有他的传说。

       FFmpeg源代码结构图 - 编码:
https://blog.csdn.net/leixiaohua1020/article/details/44226355
<https://blog.csdn.net/leixiaohua1020/article/details/44226355>

       FFmpeg源代码结构图 - 解码:
https://blog.csdn.net/leixiaohua1020/article/details/44220151
<https://blog.csdn.net/leixiaohua1020/article/details/44220151>

 

一.ffmpeg开发入门

      下面是一个打开视频的小例子。

     先用Win32控制台程序来讲解ffmpeg的简单开发,建立Win32的控制台项目,在项目属性中加入ffmpeg的库文件。没有
ffmpeg3.2.4库文件 <https://download.csdn.net/download/yao_hou/10455210>的同学,请点击下载。

      代码如下:
// FFmpeg_打开视频文件.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include
<iostream> extern "C" { #include <libavformat/avformat.h> } #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); } }
   有些变量的定义,这里不做指出,需要源码的请点击下载
<https://download.csdn.net/download/yao_hou/10455181>。

  读帧的实现如下:
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 <https://blog.csdn.net/linweig/article/details/5515928>,
RGB和色度的四通道, 我们需要把它转化成RGB进行显示。

(3)视频显示     

       视频的显示用OpenGLWidget显示,把每一帧当做图片来处理,即可显示。关于OpenGLWidget如何显示图片
<https://blog.csdn.net/yao_hou/article/details/80316739>
,请查看我给出的方法。下列代码是进行显示,解码后的视频是四通道,所以在给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(); }
       当然,在打开界面时,就让子线程运行,但由于视频没有打开,就会一直出阻塞状态,当添加视频文件后,子线程继续运行。画面也就显示了。

       效果如下:

       

       只有画面没有音频,而且画面刷新很快,这是由于只解码了视频,没有关音频。下篇进行解码音频。

       本篇的源码,请点击【源码下载 <https://download.csdn.net/download/yao_hou/10455181>
】。很多ffmpeg的API不懂的,请自行百度深入研究。

       tips:

             如果我的VS Qt项目你编译不了,请参考以下链接:

             https://blog.csdn.net/yao_hou/article/details/84302372
<https://blog.csdn.net/yao_hou/article/details/84302372>

             一键解决

          

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信