您当前的位置: 首页 >  qt

Qt+FFmpeg解视频帧转为QImage

发布时间:2022-09-21 13:53:01 ,浏览量:5

环境: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"<            
关注
打赏
1688896170
查看更多评论

暂无认证

  • 5浏览

    0关注

    115984博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0433s