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

FFmpeg 4.x 从入门到精通(一)—— QT 中如何用 FFmpeg 实现软件解码背景因为在2021年给自己定了目标和计划,学习ffmpeg,所以这篇文章是实现计划的第一步。ffmpeg众所周知,就不展开介绍了,下面给出FFmpeg4.2windowsx64lib库和头文件的下载地址(粉丝免积分下载):https://download.csdn.net/download/u012534831/14045436本文也是属于博主的入门学习总结与分享,因此我们先从ffmpeg的软解码开始,从解码到绘制,一起体验下亲自动手的快乐。本文的语言环境基于C++,界面部分是QT。

大家好,又见面了,我是你们的朋友全栈君。

背景

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

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

https://download.csdn.net/download/u012534831/14045436

本文也是属于博主的入门学习总结与分享,因此我们先从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);
}

如有兴趣,欢迎加入我的QQ群,QT/Android/音视频 问题在线解答,资源分享。
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/153390.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • java jbpm工作流_jbpm工作流

    java jbpm工作流_jbpm工作流一、JBPM(javabusinessprocessmanager)1、工作流管理流程O—>定义工作流(使用流程设计器生成,png和xml文件,分别面向用户和系统)—>执行工作流(核心对象:流程引擎ProcessEngine)—>连接数据库(jbpm18张表,jbpm4_deploymen,jbpm4_deployprop,jbpm4_execution,jbp…

  • pytest运行_怎么清理ios文件app的缓存

    pytest运行_怎么清理ios文件app的缓存前言pytest运行完用例之后会生成一个.pytest_cache的缓存文件夹,用于记录用例的ids和上一次失败的用例。方便我们在运行用例的时候加上–lf和–ff参数,快速运行上一

  • 2018最新手机号码正则表达式

    2018最新手机号码正则表达式2017年8月,工信部给三大运营商批准了新号段,中国电信获得199号段,中国移动得到198号段,中国联通得到166号段。/***正则:手机号(精确)*&lt;p&gt;移动:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、178、182、183、184、187、188、198&lt;/p&gt;…

  • centos7.6安装docker_centos docker安装部署

    centos7.6安装docker_centos docker安装部署前言前面一篇学了mac安装docker,这篇来学习在linux上安装docker环境准备Docker支持以下的CentOS版本,目前,CentOS仅发行版本中的内核支持Docker。Doc

  • 各种Oracle索引类型介绍「建议收藏」

    各种Oracle索引类型介绍「建议收藏」逻辑上:Singlecolumn单行索引Concatenated多行索引Unique唯一索引NonUnique非唯一索引Function-based函数索引Domain域索引物理上:Partitioned分区索引NonPartitioned非分区索引B-tree:Normal正常型B树ReverKey反转型B树Bitmap位图索引索引结构:B-tree:

  • mysql 分区总结[通俗易懂]

    mysql 分区总结[通俗易懂]目录简介mysql分区类型分区语法创建表与分区分区表的管理操作mysql分区表的局限性使用分区优化查询性能如何看使用到了分区可以直接指定表的分区来查询在where语句中对分区字段进行大小的限制简介数据库分区数据库分区是一种物理数据库设计技术。虽然分区技术可以实现很多效果,但其主要目的是为了在特定的SQL操作中减少数据读写的总量以缩减sq…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号