FFmpeg 4.2 从入门到精通(一)—— QT 中如何用 FFmpeg 实现软件解码

1,181 阅读5分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

背景

因为在2021年给自己定了目标和计划,学习ffmpeg,所以这篇文章是实现计划的第一步。

ffmpeg 众所周知,就不展开介绍了,下面给出 FFmpeg 4.2 windows x64 lib库和头文件的下载地址(粉丝免积分下载):

download.csdn.net/download/u0…

本文也是属于博主的入门学习总结与分享,因此我们先从ffmpeg的软解码开始,从解码到绘制,一起体验下亲自动手的快乐。本文的语言环境基于C++,界面部分是 QT。

流程分析

在开始看代码之前,我们必须先了解下ffmpeg软解的常规流程: 在这里插入图片描述

在以前的教程中我们经常见到av_regeister_all,这是旧版ffmpeg的用法,必须在开始进行初始化,新版的ffmpeg4.0之后已经不需要了,详见github: av_register_all() has been deprecated in ffmpeg 4.0

1、avformat_open_input

为 AVFormatContext 分配空间,打开输入的视频数据并且探测视频的格式,这个函数里面包含了复杂的格式解析与探测算法,可解析的内容包括:视频流、音频流、视频流参数、音频流参数、视频帧索引等。用雷神的话说就是 可以算作FFmpeg的“灵魂”

2、avformat_find_stream_info

获取多媒体流的信息,包括码流、帧率、时长等信息。但是有些早期格式或者裸流数据它的索引并没有放到头当中,因此需要在后面进行探测。注意一个视频文件中可能会同时包括视频文件、音频文件、字幕文件等多个媒体流。

3、av_find_best_stream

当视频被解封装出来后,需要分开处理音频和视频,需要找到对应的音频流和视频流,获取音视频对应的stream_index。

4、avcodec_find_decoder(enum AVCodecID id)

"Find a registered decoder with a matching codec ID." 上一步找到的AVStream中的成员变量 codecpar->codec_id 就是这儿的参数 ID,codecpar类型为AVCodecParameters。网上的很多资料为 AVCodecContext->codec_id,这个用法在FFMPEG3.4及以上版本已经被弃用了,官方推荐使用codecpar。

5、avcodec_alloc_context3

创建AVCodecContext并分配空间。

6、avcodec_parameters_to_context

该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中,执行真正的内容拷贝。avcodec_parameters_to_context()是新的API,替换了旧版本的avcodec_copy_context()。

7、avcodec_open2

用给定的 AVCodec 去初始化 AVCodecContext。

到这儿,解码器的初始化工作已经完成。下面就可以开始真正的解码操作了。

8、av_read_frame

读取码流中的音频若干帧或者视频一帧,av_read_frame()函数是新型ffmpeg的用法,对 av_read_packet 进行了封装,旧用法之所以被抛弃,就是因为以前获取的数据可能不是完整的,而av_read_frame()保证了视频数据一帧的完整性,使读出的数据总是完整的一帧。

8、avcodec_send_packet

发送数据到后台解码队列。

It can be NULL (or an AVPacket with data set to NULL and
size set to 0); in this case, it is considered a flush
packet, which signals the end of the stream. Sending the
first flush packet will return success. Subsequent ones are
unnecessary and will return AVERROR_EOF. If the decoder
still has frames buffered, it will return them after sending

源码中关于发送一包空数据的解释: 由于ffmpeg内部会缓存帧,在av_read_frame读不到数据的时候,需要通过packet.data = NULL;packet.size = 0;给ffmpeg发送一包空数据,即再avcodec_send_packet一次,将ffmpeg里面缓存的帧全部刷出来,解决最后几帧没有解码出来的问题。

9、avcodec_receive_frame

从解码器读取帧数据,这个函数执行完后,就已经能拿到我们的帧数据了,它被存储在 AVFrame 中。 此处需要注意的是: 一般而言,一次avcodec_send_packet()对应一次avcodec_receive_frame(),但是也会有一次对应多次的情况。这个得看具体的流,并常见于音频流,会存在一个AVPacket对应多个AVFrame的情况。因此可以看我上面的流程图有两个while循环。

代码示例

//头文件

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include<thread>
extern "C"
{
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavutil/pixfmt.h"
    #include "libswscale/swscale.h"
    #include "libavdevice/avdevice.h"
    #include <libavutil/pixdesc.h>
    #include <libavutil/hwcontext.h>
    #include <libavutil/opt.h>
    #include <libavutil/avassert.h>
    #include <libavutil/imgutils.h>
}

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    void init();
    void play();
private:
    std::thread m_decodecThread;
    Ui::MainWindow *ui;
    AVFormatContext *pAVFormatCtx;
    AVCodecContext *pAVCodecCtx;
    SwsContext *pSwsCtx;
    uint8_t *pRgbBuffer;
    AVPacket packet;
    AVFrame *pAVFrame = NULL;
    AVFrame *pAVFrameRGB;
    int iVideoIndex = -1;
    QImage m_image;
    bool isFinish  =false;
    void decodec();
    signals:
    void signalDraw();
public slots:
    void slotDraw();
protected:
    void paintEvent(QPaintEvent *event) override;
};

#endif // MAINWINDOW_H

//CPP文件

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QPainter>
#include<thread>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    connect(this,&MainWindow::signalDraw,this,&MainWindow::slotDraw);
}

MainWindow::~MainWindow()
{
    delete ui;
}
void MainWindow::init()
{
  std::string file = "E:/Video/bb.mp4";
 //描述多媒体文件的构成及其基本信息
 if (avformat_open_input(&pAVFormatCtx, file.data(), NULL, NULL) != 0)
     {
         qDebug() <<"open file fail";
         avformat_free_context(pAVFormatCtx);
         return;
     }

 //读取一部分视音频数据并且获得一些相关的信息
 if (avformat_find_stream_info(pAVFormatCtx, NULL) < 0)
 {
     qDebug() <<"vformat find stream fail";
     avformat_close_input(&pAVFormatCtx);
     return;
 }
// 根据解码器枚举类型找到解码器
    AVCodec *pAVCodec;
    int ret = av_find_best_stream(pAVFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pAVCodec, 0);
    if (ret < 0) {
        qDebug()<< "av_find_best_stream faliture";
        avformat_close_input(&pAVFormatCtx);
        return;
    }
    iVideoIndex = ret;

     pAVCodec = avcodec_find_decoder(pAVFormatCtx->streams[iVideoIndex]->codecpar->codec_id);
     if (pAVCodec == NULL)
     {
         qDebug()<<"not find decoder";
         return;
     }

 qDebug()<<"avcodec_open2 pAVCodec->name:" << QString::fromStdString(pAVCodec->name);


    if(pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.den != 0) {
        float fps_ = pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.num / pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.den;
           qDebug() <<"fps:" << fps_;
    }
      int64_t video_length_sec_ = pAVFormatCtx->duration/AV_TIME_BASE;
       qDebug() <<"video_length_sec_:" << video_length_sec_;
pAVCodecCtx = avcodec_alloc_context3(pAVCodec);
     if (pAVCodecCtx == NULL)
     {
         qDebug() <<"get pAVCodecCtx fail";
         avformat_close_input(&pAVFormatCtx);
         return;
     }
ret = avcodec_parameters_to_context(pAVCodecCtx,pAVFormatCtx->streams[iVideoIndex]->codecpar);
     if (ret < 0)
     {
         qDebug() <<"avcodec_parameters_to_context fail";
         avformat_close_input(&pAVFormatCtx);
         return;
     }
  if (avcodec_open2(pAVCodecCtx, pAVCodec, NULL) < 0)
     {
         qDebug()<<"avcodec_open2 fail";
         return;
     }
         //为解码帧分配内存
         //AVFrame 存放从AVPacket中解码出来的原始数据
             pAVFrame = av_frame_alloc();
             pAVFrameRGB = av_frame_alloc();
       //用于视频图像的转换,将源数据转换为RGB32的目标数据
         pSwsCtx = sws_getContext(pAVCodecCtx->width, pAVCodecCtx->height, pAVCodecCtx->pix_fmt,
                                              pAVCodecCtx->width, pAVCodecCtx->height, AV_PIX_FMT_RGB32,
                                              SWS_BICUBIC, NULL, NULL, NULL);

        int  m_size = av_image_get_buffer_size(AVPixelFormat(AV_PIX_FMT_RGB32), pAVCodecCtx->width, pAVCodecCtx->height, 1);
      pRgbBuffer = (uint8_t *)(av_malloc(m_size));
         //为已经分配的空间的结构体AVPicture挂上一段用于保存数据的空间
         avpicture_fill((AVPicture *)pAVFrameRGB, pRgbBuffer, AV_PIX_FMT_BGR32, pAVCodecCtx->width, pAVCodecCtx->height);
         //av_image_fill_arrays
         //AVpacket 用来存放解码数据
         av_new_packet(&packet, pAVCodecCtx->width * pAVCodecCtx->height);
}


void MainWindow::play()
{
    m_decodecThread = std::thread([this]()
    {
        decodec();
    });
    m_decodecThread.detach();
}

void MainWindow::decodec()
{
    //读取码流中视频帧
        while (true)
        {
            int ret = av_read_frame(pAVFormatCtx, &packet);
            if(ret != 0)
            {
                qDebug()<<"file end";
                isFinish = !isFinish;
                 return;
            }
            if (packet.stream_index != iVideoIndex)
            {
                av_packet_unref(&packet);
                continue;
            }
           int iGotPic = AVERROR(EAGAIN);
//             //解码一帧视频数据
            iGotPic = avcodec_send_packet(pAVCodecCtx, &packet);
            if(iGotPic!=0){
                qDebug()<<"avcodec_send_packet error";
                      continue;
            }
            iGotPic = avcodec_receive_frame(pAVCodecCtx, pAVFrame);
   if(iGotPic == 0){
                   //转换像素
                   sws_scale(pSwsCtx, (uint8_t const * const *)pAVFrame->data, pAVFrame->linesize, 0, pAVCodecCtx->height, pAVFrameRGB->data, pAVFrameRGB->linesize);

                   //构造QImage
                   QImage img(pRgbBuffer, pAVCodecCtx->width, pAVCodecCtx->height, QImage::Format_RGB32);
                   qDebug()<<"decode img";
                   m_image = img;
                   emit signalDraw();
  }else {
           qDebug()<<"decode error";
           }

            av_packet_unref(&packet);
            std::this_thread::sleep_for(std::chrono::milliseconds(25));
        }
  //资源回收
        av_free(pAVFrame);
        av_free(pAVFrameRGB);
        sws_freeContext(pSwsCtx);
        avcodec_close(pAVCodecCtx);
        avformat_close_input(&pAVFormatCtx);
}

void MainWindow::slotDraw()
{
    update();
}

void MainWindow::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setBrush(Qt::black);
    painter.drawRect(0, 0, this->width(), this->height());

    if (m_image.size().width() <= 0)
        return;

    //比例缩放
    QImage img = m_image.scaled(this->size(),Qt::KeepAspectRatio);
    int x = this->width() - img.width();
    int y = this->height() - img.height();

    x /= 2;
    y /= 2;

    //QPoint(x,y)为中心绘制图像
    painter.drawImage(QPoint(x,y),img);
}