环境:Win10 + Qt5.15.2 + MSVC 64bit + FFmpeg4.4
解码流程:
图片来源: FFmpeg编程-解码流程_贝勒里恩的博客-CSDN博客_ffmpeg解码流程
其他参考:
博客:Qt+FFMPEG学习(一)视频帧转换为QImage_Italink的博客-CSDN博客_qstring转qimage
博客:使用Qt线程 + FFmpeg获取视频流并显示图像到窗口_友善啊,朋友的博客-CSDN博客_qt接收视频流
博客:FFMPEG开发快速入坑——音视频编码处理 - 知乎
遇到的小问题:
1.用 avcodec_receive_frame 获取解码后的帧数据,返回 AVERROR(EAGAIN) (值为-11),文档描述为:输出在此状态下不可用-用户必须尝试发送新输入,所以加了个 continue 循环执行 avcodec_send_packet,直到可用。
2.sws_scale (视频像素格式和分辨率转换)的输出可以直接用字节缓冲区接收,不一定非要把缓冲区 attach 到 AVFrame 上。
3.QImage、QByteArray这些容器如果直接用字节数据构造,是没有深拷贝一份的,需要自己分离。
代码链接:https://github.com/gongjianbo/LearnFFmpeg.git
主要代码:
#include "MainWindow.h" #include "ui_MainWindow.h" extern "C" { #include #include #include #include #include #include #include #include } #include#includeMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); fprintf(stderr, "ffmpeg version:%s\n", av_version_info()); connect(ui->pushButton, &QPushButton::clicked, this, [this]{ const QString &filepath = QFileDialog::getOpenFileName(this); qDebug()<<"select filepath:"<label->setPixmap(QPixmap::fromImage(getFrameImage(filepath, 100))); }); } MainWindow::~MainWindow() { delete ui; } //用于资源释放 struct AVGuard { //格式化I/O上下文 AVFormatContext *formatCtx = NULL; //解码器对应的流 int streamIndex = -1; //解码器 AVCodec *codec = NULL; //解码器上下文 AVCodecContext *codecCtx = NULL; //参数信息 AVCodecParameters *codecParam = NULL; //AVPacket存储压缩数据 //视频通常包含一个压缩帧,音频可能包含多个压缩帧 AVPacket *packet = NULL; //AVFrame存储原始数据,YUV、RGB、PCM等 //转换前图像 AVFrame *frameIn = NULL; //转换后图像 AVFrame *frameOut = NULL; uint8_t *outBuf = NULL; //转换 SwsContext *swsCtx = NULL; ~AVGuard() { qDebug()<<"free"; if(codecCtx){ //avcodec_close(codecCtx); avcodec_free_context(&codecCtx); codecCtx = NULL; } if(formatCtx){ avformat_close_input(&formatCtx); avformat_free_context(formatCtx); formatCtx = NULL; } codec = NULL; codecParam = NULL; if(frameIn){ av_frame_unref(frameIn); av_frame_free(&frameIn); frameIn = NULL; } if(frameOut){ av_frame_unref(frameOut); av_frame_free(&frameOut); frameOut = NULL; } if(outBuf){ av_free(outBuf); outBuf = NULL; } if(packet){ av_packet_unref(packet); av_packet_free(&packet); packet = NULL; } if(swsCtx){ sws_freeContext(swsCtx); swsCtx = NULL; } } }; QImage MainWindow::getFrameImage(const QString &filepath, int pos) { //参考:https://blog.csdn.net/qq_40946921/article/details/115794514 //参考:https://blog.csdn.net/kenfan1647/article/details/123687910 //参考:https://zhuanlan.zhihu.com/p/346010443 QImage image; //借助析构函数来释放 AVGuard guard; //打开输入流并读取头 //流要使用avformat_close_input关闭,成功时返回=0 int result = avformat_open_input(&guard.formatCtx, filepath.toUtf8().constData(), NULL, NULL); if (result != 0 || guard.formatCtx == NULL) return image; //读取文件获取流信息,把它存入AVFormatContext中 //正常时返回>=0 if (avformat_find_stream_info(guard.formatCtx, NULL) < 0) return image; //时长,duration/AV_TIME_BASE单位为秒 qDebug()<<"时长"<duration/double(AV_TIME_BASE)<<"s"; qDebug()<<"格式"<iformat->name; //找到视频流,获取对应解码器 //for (unsigned int i = 0; i < guard.formatCtx->nb_streams; i++) { // if(guard.formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { guard.streamIndex = av_find_best_stream(guard.formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (guard.streamIndex < 0) return image; //参数信息 guard.codecParam = guard.formatCtx->streams[guard.streamIndex]->codecpar; //查找具有匹配编解码器ID的已注册解码器 //失败返回NULL guard.codec = avcodec_find_decoder(guard.codecParam->codec_id); if (!guard.codec) return image; //AVStream.codec属性已弃用,这里使用avcodec_alloc_context3创见一个编解码上下文 guard.codecCtx = avcodec_alloc_context3(guard.codec); //流参数复制到CodecCtx avcodec_parameters_to_context(guard.codecCtx, guard.codecParam); //打开解码器 //正常时返回0 if (!guard.codecCtx || avcodec_open2(guard.codecCtx, guard.codec, NULL) != 0) return image; qDebug()<<"解码器"<name; qDebug()<<"宽*高"<width<height; //跳转到某一帧 //参数一: 上下文; //参数二: 流索引, 如果stream_index是-1,会选择一个默认流,时间戳会从以AV_TIME_BASE为单位向具体流的时间基自动转换。 //参数三: 将要定位处的时间戳,time_base单位或者如果没有流是指定的就用av_time_base单位。 //参数四: seek功能flag; //AVSEEK_FLAG_BACKWARD 是seek到请求的timestamp之前最近的关键帧 //AVSEEK_FLAG_BYTE 是基于字节位置的查找 //AVSEEK_FLAG_ANY 是可以seek到任意帧,注意不一定是关键帧,因此使用时可能会导致花屏 //AVSEEK_FLAG_FRAME 是基于帧数量快进 //返回值:成功返回>=0 if (av_seek_frame(guard.formatCtx, guard.streamIndex, pos * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD) < 0) return image; //跳转后把解码器的数据清空下 if (guard.codecCtx) { avcodec_flush_buffers(guard.codecCtx); } //AVPacket存储压缩数据 guard.packet = av_packet_alloc(); //AVFrame存储原始数据,YUV、RGB、PCM等 //存放输入 guard.frameIn = av_frame_alloc(); //存放输出 guard.frameOut = av_frame_alloc(); //返回存储给定参数的图像数据所需数据量的大小(以字节为单位) int size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, guard.codecParam->width, guard.codecParam->height, 1); guard.outBuf = static_cast(av_malloc(static_cast(size))); //也可以不用frame放输出,直接用buffer+linesize //int outLineSize[3]; //构造AVFrame到QImage所需要的数据 //av_image_fill_linesizes(outLineSize, AV_PIX_FMT_RGB24, guard.codecParam->width); //根据后5个参数的内容填充前两个参数,成功返回源图像的大小,失败返回一个负值 if (av_image_fill_arrays(guard.frameOut->data, guard.frameOut->linesize, guard.outBuf, AV_PIX_FMT_RGB24, guard.codecParam->width, guard.codecParam->height, 1) < 0) return image; //指定sws_scale上下文 guard.swsCtx = sws_getContext(guard.codecParam->width, guard.codecParam->height, guard.codecCtx->pix_fmt, guard.codecParam->width, guard.codecParam->height, AV_PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); if (!guard.swsCtx) return image; while (true) { //读取码流中的一帧视频,或者若干帧音频 //注:av_read_frame 每次循环后必须执行av_packet_unref(packet)进行释放 //frame同理 if(av_read_frame(guard.formatCtx, guard.packet) != 0) return image; if (guard.packet->stream_index == guard.streamIndex) { //发送编码数据包,将一个packet放入到队列中等待解码 //注:ffmpeg内部会缓冲几帧,要想取出来就需要传递空的AVPacket进去 result = avcodec_send_packet(guard.codecCtx, guard.packet); //当前状态下不接受输入-用户必须使用avcodec_receive_frame()读取输出 if (result == AVERROR(EAGAIN)) { avcodec_receive_frame(guard.codecCtx, guard.frameIn); av_packet_unref(guard.packet); av_frame_unref(guard.frameIn); continue; } else if (result != 0) { return image; } //接收解码后数据,将解码后的数据拷贝给avframe //avcodec_send_packet和avcodec_receive_frame调用关系并不一定是一一对应 //如一些音频数据调用一次avcodec_send_packet之后, //可能需要调用多次avcodec_receive_frame才能获取全部的解码音频数据 result = avcodec_receive_frame(guard.codecCtx, guard.frameIn); //输出在此状态下不可用-用户必须尝试发送新输入 if (result == AVERROR(EAGAIN)) { av_packet_unref(guard.packet); av_frame_unref(guard.frameIn); continue; } else if (result != 0) { return image; } //视频像素格式和分辨率的转换 //函数功能:1.图像色彩空间转换;2.分辨率缩放;3.前后图像滤波处理。 //效率相对较低,不如libyuv或shader sws_scale(guard.swsCtx, static_cast(guard.frameIn->data), guard.frameIn->linesize, 0, guard.codecParam->height, guard.frameOut->data, guard.frameOut->linesize); //guard.frameOut->data就是指向的guard.outBuf,可替换 image = QImage(static_cast(guard.frameOut->data[0]), guard.codecParam->width, guard.codecParam->height, QImage::Format_RGB888); //构造用的buf的内存,这里拷贝一份 image = image.copy(); qDebug()<<"finish"<关注打赏