13.QT-ffmpeg4.4显示视频图像
在上章12.QT-通过QOpenGLWidget显示YUV画面,通过QOpenGLTexture纹理渲染YUV,我们学会了如何硬解码,但是ffmpeg图像解码过程还不知道.所以本章主要分析一下FFmpeg视频图像解码过程,只有真正了解了FFmpeg处理的基本流程,研读 ffmpeg 源代码才能事半功倍.笔者使用的ffmpeg版本为4.4.
1.FFmpeg库简介
FFmpeg常用库如下:
- avcodec : 用于各种类型声音/图像编解码(最重要的库),该库是音视频编解码核心
- avformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;音视频的格式解析协议,为 avcodec分析码流提供独立的音频或视频码流源
- avfilter : 滤镜特效处理, 如宽高比 裁剪 格式化 非格式化 伸缩。
- avdevice:各种硬件采集设备的输入输出。
- avutil:工具库,包括算数运算字符操作(大部分库都需要这个库的支持)
- postproc:用于后期效果处理;音视频应用的后处理,如图像的去块效应。
- swresample:音频采样数据格式转换。
- swscale:视频像素数据格式转换、如 rgb565、rgb888 等与 yuv420 等之间转换。
2.FFmpeg结构体
ffmpeg结构体关系图如下所示,用的雷神图,可以看到很多结构体都与AVFormatContext有关系,所以很多函数也经常使用AVFormatContext作为参数使用(FFmpeg旧版本):
注意:在新版本中,AVCodecContext已经和AVStream[]做了改进,已经分解开了,比如推流的时候,接完封装就发送了,没必要解码,而远端用户才是做接收,解码流程。
- AVFormatContext : 存储视音频封装格式(flv,mp4,rmvb,avi)中包含的所有信息.通过avformat_open_input()来分配空间并打开文件,结束时通过avformat_close_input()来释放.
- AVIOContext : 存在AVFormatContext ->pb中,用来存储文件数据的缓冲区,并通过相关标记成员来实现文件读写操作,其中的opaque 成员这是用于关联 URLContext 结构
- URLContext : 存在AVIOContext->opaque中,表示程序运行的当前广义输入文件使用的 context,着重于所有广义输入文件共有的属性(并且是在程序运行时才能确定其值)和关联其他结构的字段.
- URLProtocol : 存在URLContext-> prot中,音视频输入文件类型(rtp,rtmp,file, rtmps, udp等),比如file类型的结构体初始化如下:
- AVInputFormat : 存在AVFormatContext ->iformat中, 保存视频/音频流的封装格式(flv、mkv、avi等),其中name成员可以查看什么格式
- AVStream: 视音频流,存在AVFormatContext->streams[i], 每个AVStream包含了一个流,一般默认两个(0为视频流,1为音频流).通过avformat_find_stream_info()来获取.
- AVCodecContext: 用来保存解码器上下文结构体(保存解码相关信息,主要存储在程序运行时才能确定的数据),每个AVCodecContext包含了一个AVCodec解码器(比如h.264解码器、mpeg4解码器等),老版本存在AVFormatContext->streams[i] ->codec中,新版本则需要通过avcodec_alloc_context3()和avcodec_parameters_to_context()函数来构造AVCodecContext.
- AVCodec : 存在AVCodecContext->codec中,指定具体的解码器(比如h.264解码器、mpeg4解码器等),。
- AVPacket : 解码前的音频/视频数据,需要用户自己分配空间后才通过av_read_frame()来获取一帧未解码的数据
- AVFrame : 解码后的音频/视频数据,比如解码视频数据则通过avcodec_receive_frame()来获取一帧AVFrame数据
上面部分的结构体封装参考连接如下:
剩下的还有AVIOContext、URLContext、URLProtocol 、结构体未分析,后续使用时补充
3.解码流程(针对只显示一个简单的视频画面,后续同步声音)
如下图所示,针对ffmpeg4.4版本:
- 如果avformat_open_input()打开的是http,rtsp,rtmp,mms网络相关的流媒体,则需要在开头调用avformat_network_init()来为提供支持.
4.AVPacket使用注意
- AVPacket必须使用av_packet_allc()创建好空间后.才能供给fimpeg进行获取解码前帧数据,由于解码前帧数据大小是不固定的(比如I帧数据量最大)所以ffmpeg会在AVPacket的成员里动态进行创建空间.
- 并且我们每一次使用完AVPacket后(再次调用av_read_frame()读取新帧之前),必须要通过av_packet_unref()引用技术对AVPacket里的成员来手动清理.
- 解码完成或者退出播放后,还要调用av_packet_free()来释放AVPacket本身.
5.AVFrame使用注意
- AVFrame必须使用av_frame_alloc()来分配。注意,这只是分配AVFrame本身,缓冲区的数据(解码成功后的数据)必须通过其他途径被管理.
- 因为AVFrame通常只分配一次,然后多次复用来保存不同类型的数据,复用的时候需要调用av_frame_unref()将其重置到它前面的原始清洁状态.
- 注意调用avcodec_receive_frame()时会自动引用减1后再获取frame,所以解码过程中无需每次调用
- 释放的时候必须用av_frame_free()释放。
6.SwsContext结构体介绍(转换格式与尺寸)
由于我们是软解,需要的图像格式是RGB类型的,所以我们需要将YUV格式进行转换,而ffmpeg库中提供了一个SwsContext类,该类主要用于图片像素格式的转换, 图片的尺寸改变.
SwsContext常用相关函数如下所示:
SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param); // sws_getContext通过参数图像转换格式以及分辨率来初始化SwsContext结构体 //srcW, srcH, srcFormat定义输入图像信息(宽、高、颜色空间(像素格式)) //dstW, dstH, dstFormat定义输出图像信息(宽、高、颜色空间(像素格式,比如AV_PIX_FMT_RGB32))。 // flags:转换算法(只有当输入输出图像大小不同时有效,速度越快精度越差,一般选择SWS_BICUBIC) // *srcFilter,* dstFilter: 定义输入/输出图像滤波器信息,一般输入NULL // param定义特定缩放算法需要的参数,默认为NUL //比如sws_getContext(w, h, AV_PIX_FMT_YUV420P, 2*w, 2*h, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); 表示YUV420p-> RGB32,并放大4倍 struct SwsContext *sws_getCachedContext(struct SwsContext *context, int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param); //检查context参数是否可以重用,否则重新分配一个新的SwsContext。 比sws_getContext()多了一个context参数 //比如我们当前传入的srcW、srcH、srcFormat、dstW、dstH、dstFormat、param参数和之前的context一致,则就直接返回复用. //否则的话,释放context,并重新初始化一个新的SwsContext返回. //如果要转换的视频尺寸和格式始终不变(期间不更改),一般使用sws_getContext() int sws_scale(struct SwsContext *c, const uint8_t * const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[]); // sws_scale用来进行视频像素格式和分辨率的转换.返回值小于0则表示转换失败 //注意:使用之前需要调用sws_getContext()来设置像素转换格式,并SwsContext结构体,并且用后还要调用sws_freeContext()来释放SwsContext结构体. //*c:转换格式的上下文,里面保存了要转换的格式和分辨率 //*srcSlice[]:源图像数据,也就是解码后的AVFrame-> data[]数组成员,需要注意的是里面的每一行像素并不等于图片的宽度 // srcStride[]: input的 strid,每一列图像的byte数,也就是AVFrame->linesize成员 // srcSliceY, srcSliceH: srcSliceY是起始位置,srcSliceH是处理多少行,如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。也可以多线程并行加快速度显示,例如第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。 // dst[]:转换后的图像数据,也就是要转码后的另一个AVFrame-> data[]成员 // dstStride[]: input的 strid,每一列图像的byte数,也就是要转码后的另一个AVFrame->linesize成员 void sws_freeContext(struct SwsContext *swsContext); //释放swsContext结构体,避免内存泄漏,释放后用户还需要手动置NULL
7.源码
#include "ffmpegtest.h" #include <QTime> #include <QDebug> extern "C"{ #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> } FfmpegTest::FfmpegTest(QWidget *parent) : QWidget(parent) { ui->setupUi(this); } void Delay(int msec) { QTime dieTime = QTime::currentTime().addMSecs(msec); while( QTime::currentTime() < dieTime ) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } } void debugErr(QString prefix, int err) //根据错误编号获取错误信息并打印 { char errbuf[512]={0}; av_strerror(err,errbuf,sizeof(errbuf)); qDebug()<<prefix<<":"<<errbuf; } void FfmpegTest::on_pushButton_clicked() { AVFormatContext *pFormatCtx; int videoindex; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame, *pFrameRGB; unsigned char *out_buffer; AVPacket *packet; int ret; struct SwsContext *img_convert_ctx; char filepath[] = "G:\\testvideo\\ds.mov"; avformat_network_init(); //加载socket库以及网络加密协议相关的库,为后续使用网络相关提供支持 pFormatCtx = avformat_alloc_context(); //初始化AVFormatContext 结构 ret=avformat_open_input(&pFormatCtx, filepath, NULL, NULL);//打开音视频文件并初始化AVFormatContext结构体 if (ret != 0) { debugErr("avformat_open_input",ret); return ; } ret=avformat_find_stream_info(pFormatCtx, NULL);//根据AVFormatContext结构体,来获取视频上下文信息,并初始化streams[]成员 if (ret != 0) { debugErr("avformat_find_stream_info",ret); return ; } videoindex = -1; videoindex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);//根据type参数从ic-> streams[]里获取用户要找的流,找到成功后则返回streams[]中对应的序列号,否则返回-1 if (videoindex == -1){ printf("Didn't find a video stream.\n"); return ; } qDebug()<<"视频宽度:"<<pFormatCtx->streams[videoindex]->codecpar->width; qDebug()<<"视频高度:"<<pFormatCtx->streams[videoindex]->codecpar->height; qDebug()<<"视频码率:"<<pFormatCtx->streams[videoindex]->codecpar->bit_rate; ui->label->resize(pFormatCtx->streams[videoindex]->codecpar->width,pFormatCtx->streams[videoindex]->codecpar->height); pCodec = avcodec_find_decoder(pFormatCtx->streams[videoindex]->codecpar->codec_id);//通过解码器编号来遍历codec_list[]数组,来找到AVCodec pCodecCtx = avcodec_alloc_context3(pCodec); //构造AVCodecContext ,并将vcodec填入AVCodecContext中 avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar); //初始化AVCodecContext ret = avcodec_open2(pCodecCtx, NULL,NULL); //打开解码器 if (ret != 0) { debugErr("avcodec_open2",ret); return ; } //构造AVFrame,而图像数据空间大小则需通过av_malloc动态分配(因为不知道视频宽高大小) pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); //创建动态内存,创建存储图像数据的空间 //av_image_get_buffer_size():根据像素格式、图像宽、图像高来获取一帧图像需要的大小(第4个参数align:表示多少字节对齐,一般填1,表示以1字节为单位) //av_malloc():给out_buffer分配一帧RGB32图像显示的大小 out_buffer = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, 1)); //通过av_image_fill_arrays和out_buffer来初始化pFrameRGB里的data指针和linesize指针.linesize是每个图像的宽大小(字节数)。 av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, out_buffer, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, 1); packet = (AVPacket *)av_malloc(sizeof(AVPacket)); //初始化SwsContext结构体,设置像素转换格式规则,将pCodecCtx->pix_fmt格式转换为AV_PIX_FMT_RGB32格式 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); //av_read_frame读取一帧未解码的数据,可能还有几帧frame未显示,我们需要在末尾通过avcodec_send_packet()传入NULL来将最后几帧取出来 while (av_read_frame(pFormatCtx, packet) >= 0){ //如果是视频数据 if (packet->stream_index == videoindex){ //解码一帧视频数据 ret = avcodec_send_packet(pCodecCtx, packet); av_packet_unref(packet); if (ret != 0) { debugErr("avcodec_send_packet",ret); continue ; } //调用avcodec_receive_frame()时会自动引用减1后再获取frame,所以解码过程中无需每次调用av_frame_unref()来重置AVFrame while( avcodec_receive_frame(pCodecCtx, pFrame) == 0){ qDebug()<<"视频帧类型(I(1)、B(3)、P(2)):"<<pFrame->pict_type; //进行视频像素格式和分辨率的转换 sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); QImage img((uchar*)pFrameRGB->data[0],pCodecCtx->width,pCodecCtx->height,QImage::Format_ARGB32); ui->label->setPixmap(QPixmap::fromImage(img)); Delay(40); } } } av_packet_free(&packet); av_frame_free(&pFrameRGB); av_frame_free(&pFrame); sws_freeContext(img_convert_ctx); img_convert_ctx=NULL; avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); }
其中几个参数较多的函数声明解释如下所示:
int avformat_open_input(AVFormatContext **ps, const char *filename, ff_const59 AVInputFormat *fmt, AVDictionary **options); //打开一个音视频文件,并初始化AVFormatContext **ps(如果初始化失败,则释放ps,并返回非0值) //ps:要初始化的AVFormatContext*的指针,如果ps指向NULL,则该函数内部会调用avformat_alloc_context()来自动分配空间。 //*url:传入的地址, 支持http,RTSP,以及普通的本地文件,初始化后地址会存放在AVFormatContext下的url成员中 //fmt: 指定输入的封装格式。默认为NULL,由FFmpeg自行探测。 // options: 其它参数设置,如果打开的是本地文件一般为NULL,比如打开流媒体视频时,通过av_dict_set(&pOptions, "max_delay", "200", 0)来设置网络延时最大200毫秒,具体参考libavformat/options_table.h下的 avcodec_options数组 //返回值:0成功,非0失败,失败后,则可以通过av_strerror()获取失败原因。 int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options); //根据AVFormatContext结构体,来获取视频上下文信息,并初始化streams[]成员(如果初始化失败,则释放ps,并返回非0值) //ic: AVFormatContext。 //options:其它参数设置,一般为NULL int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags); //根据type参数从ic-> streams[]里获取用户要找的流,找到成功后则返回streams[]中对应的序列号 //ic: AVFormatContext结构体句柄 //type:要找的流参数,比如: AVMEDIA_TYPE_VIDEO,AVMEDIA_TYPE_AUDIO,AVMEDIA_TYPE_SUBTITLE等 wanted_stream_nb: 用户希望请求的流号,设为-1用于自动选择 related_stream: 试着找到一个相关的流(比如多路视频时),一般设为-1,表示不需要 decoder_ret:如果非空,则返回所选流的解码器(相当于调用avcodec_find_decoder()函数) flags:未定义
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); //通过avctx打开解码器, AVCodecContext存在AVFormatContext->streams[i] ->codec中 //如果在这之前调用了avcodec_alloc_context3(vcodec)初始化了avctx,那么codec可以填NULL. // options: 其它参数设置,具体参考libavformat/options_table.h下的 avcodec_options数组
人间有真情,人间有真爱。