基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)

基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)

本文参考了雷博士的博客:
最简单的基于FFmpeg的视频编码器-更新版(YUV编码为HEVC(H.265))

还参考了另一篇博客:
Qt与FFmpeg联合开发指南(三)——编码(1):代码流程演示

在为了代码简洁,代码中还用到了 Qt 。先不讲解具体的实现代码。大家先看看我封装后的类的使用方法。下面是一个简单的例子。这个例子先生成了一些 QImage 图像。然后把这些图像插入到视频中。

#include <QCoreApplication>
#include <QPainter>
#include <QDebug>
#include "VideoRecorder.h"

void paint(QImage &image, int i)
{
    QPainter p(&image);
    image.fill(Qt::white);
    p.drawPie(50 + i, 100, 100, 100, 0, 16*360);
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Qly::VideoRecorder writer;
    writer.setAVCodecID(AV_CODEC_ID_MPEG4);
    writer.setTimeBase(AVRational({1, 25}));
    writer.openFile("D:\\MPEG4.avi");
    QImage image(QSize(1024, 768), QImage::Format_RGB32);
    for(int i = 0; i < 1500; i++)
    {
        paint(image, i);
        writer.setImage(image, i * 1);
    }
    writer.close();
    qDebug() << "finish";
    return a.exec();
}

可以看到上门的代码的核心就是 Qly::VideoRecorder 这个类。我们所有的视频编码相关的代码都封装在里面了。

视频编码大体可以分成这么几步:

  1. 打开视频文件,做必要的准备
  2. 图像写入 Frame
  3. Frame 转换成 Packet
  4. Packet 写入文件中
  5. 写文件尾,关闭文件

下面也分这么几步来介绍。

打开视频文件,做必要的准备

首先在建立这个类的时候要初始化几个指针。

VideoRecorder::VideoRecorder(QObject *parent) : QObject(parent)
{
    m_pFormatCtx = nullptr;
    m_pPacket = av_packet_alloc();
    if(!m_pPacket)
    {
        qWarning() << "VideoRecorder::VideoRecorder av_packet_alloc failed.";
    }
    m_pFrame = av_frame_alloc();
    if (!m_pFrame)
    {
        qWarning() << "VideoRecorder::VideoRecorder av_frame_alloc failed.";
    }
}

之后是打开文件的操作:

int VideoRecorder::openFile(QString url)
{
    m_startTime = QTime(); //将 m_startTime 复原到原始状态
    if(url.isNull() || url.isEmpty())
    {
        qWarning() << "VideoRecorder::openFile failed, url is Invalid(empty)";
        return -1;
    }
    m_url = url;
    if(m_pFormatCtx)
    {
        avformat_free_context(m_pFormatCtx);
    }
    m_errorcode = avformat_alloc_output_context2(&m_pFormatCtx, nullptr,
                                               nullptr,
                                               url.toLocal8Bit().constData());
    if(m_errorcode < 0)
    {
        qWarning() << "In VideoRecorder::openFile avformat_alloc_output_context2 failed";
        return -2;
    }
    qDebug() << "avformat_alloc_output_context2 success";
    if (!(m_pFormatCtx->flags & AVFMT_NOFILE))
    {
        m_errorcode = avio_open(&m_pFormatCtx->pb, m_url.toLocal8Bit().constData(), AVIO_FLAG_READ_WRITE);
        if(m_errorcode < 0)
        {
            qWarning() << "in VideoRecorder::openFile avio_open failed";
            return -3;
        }
    }
    qDebug() << "avio_open success";
    m_recording = true;
    return 0;
}

可以看到这里的代码也不多。因为我们还不知道图像的尺寸。所以没法设置CodecContext 。这部分操作要等到第一帧图像插入的时候才能做。

当我们确定好视频编码格式还有图像的尺寸后,就可以初始化AVStream 和 AVCodec 了。下面是相应的代码。

void VideoRecorder::initStreamParameters(AVStream * stream)
{
    stream->time_base.den = m_time_base.den;
    stream->time_base.num = m_time_base.num;
    stream->id = m_pFormatCtx->nb_streams -1;
    stream->index = m_pFormatCtx->nb_streams -1;
    stream->codecpar->codec_tag = 0;
    stream->codecpar->codec_type = m_pCodec->type;
    stream->codecpar->codec_id = m_pCodec->id;
    stream->codecpar->format = m_format;
    stream->codecpar->width = m_width;
    stream->codecpar->height = m_height;
    stream->codecpar->bit_rate = m_bit_rate;
}

int VideoRecorder::initFile(AVCodecID codecID, QSize size)
{
    qDebug() << "IN VideoRecorder::initFile";
    m_width = size.width();
    m_height = size.height();
    m_codecID = codecID;
    m_pCodec = avcodec_find_encoder(codecID);

    if (!m_pCodec)
    {
        qWarning() << "VideoRecorder::initFile avcodec_find_encoder failed.";
        return -2;
    }
    qDebug() << "avcodec_find_encoder success, codecID = " << codecID ;
    AVStream *pStream = avformat_new_stream(m_pFormatCtx, m_pCodec);
    if(pStream == nullptr)
    {
        qWarning() << "VideoRecorder::initFile avformat_new_stream failed.";
        return -3;
    }
    qDebug() << "avformat_new_stream success";

    initStreamParameters(pStream);
    //m_pCodecCtx = pStream->codec;

    qDebug() << "initStreamParameters success";
    if(m_pCodecCtx)
    {
        qDebug() << "avcodec_free_context";
        avcodec_free_context(&m_pCodecCtx);
    }

    qDebug() << "m_pCodecCtx = " << m_pCodecCtx;
    m_pCodecCtx = avcodec_alloc_context3(m_pCodec);
    if(!m_pCodecCtx)
    {
       qWarning() << "VideoRecorder::initFile avcodec_alloc_context3 failed.";
       return -4;
    }
    qDebug() << "avcodec_alloc_context3 success";
    m_pCodecCtx->codec_id = m_pCodec->id;
    m_pCodecCtx->time_base = pStream->time_base;
    m_pCodecCtx->gop_size = 10;
    m_pCodecCtx->max_b_frames = 0;

    //qDebug() << "max_b_frames";

    if (codecID == AV_CODEC_ID_H264)
    {
     av_opt_set(m_pCodecCtx->priv_data, "preset", "fast", 0);
     //av_opt_set(pCodecCtx->priv_data, "tune", "zerolatency", 0);
     //av_opt_set(pCodecCtx->priv_data, "profile", "main", 0);

    }
    else if(codecID == AV_CODEC_ID_H265)
    {
     av_opt_set(m_pCodecCtx->priv_data, "preset", "fast", 0);
     //av_opt_set(pCodecCtx->priv_data, "tune", "zerolatency", 0);
     //av_opt_set(pCodecCtx->priv_data, "profile", "main", 0);
    }

    qDebug() << "av_opt_set";
    /* Some formats want stream headers to be separate. */
    if (m_pFormatCtx->oformat->flags & AVFMT_GLOBALHEADER)
    {
     m_pFormatCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    avcodec_parameters_to_context(m_pCodecCtx, pStream->codecpar);
    m_errorcode = avcodec_open2(m_pCodecCtx, m_pCodec, nullptr);
    if(m_errorcode < 0)
    {
        qWarning() << "VideoRecorder::initFile avcodec_open2 failed.";
        return -5;
    }
    qDebug() << "avcodec_open2 success";
    m_pFrame->format = (int)m_pCodecCtx->pix_fmt;
    m_pFrame->width  = m_pCodecCtx->width;
    m_pFrame->height = m_pCodecCtx->height;

    if( av_frame_get_buffer(m_pFrame, 0) < 0 )
    {
        qWarning() << "VideoRecorder::initFile av_frame_get_buffer() failed.";
        return -6;
    }
     qDebug() << "av_frame_get_buffer success";
    return 0;
}
int VideoRecorder::writeHeader()
{
    m_errorcode = avformat_write_header(m_pFormatCtx, nullptr);
    if(m_errorcode < 0)
    {
        qWarning() << "in VideoRecorder::writeHeader avformat_write_header failed";
        return -2;
    }
    return 0;
}

图像写入 Frame

这部分比较简单。我就实现了一个功能,把 QImage 转换成 AVFrame。

void VideoRecorder::buildFrameFromImage(AVFrame *pFrame, const QImage &image, int pts)
{
    //qDebug() << "IN VideoRecorder::buildFrameFromImage";
    /* make sure the frame data is writable */
    if (av_frame_make_writable(pFrame) < 0)
    {
        qWarning() << "in VideoRecorder::buildFrameFromImage av_frame_make_writable(pFrame) failed";
        return;
    }

    int width = image.width();
    int height = image.height();
    AVPixelFormat imgFmt = toAVPixelFormat(image.format());
    SwsContext * pContext = sws_getContext(width, height, imgFmt,
                                           width, height, (AVPixelFormat)pFrame->format, SWS_POINT, nullptr, nullptr, nullptr);
    if(!pContext) return;

    const uint8_t *in_data[1];
    int in_linesize[1];

    in_data[0] = image.bits();
    in_linesize[0] = image.bytesPerLine();

    sws_scale(pContext, in_data, in_linesize, 0, height,
              pFrame->data, pFrame->linesize);
    sws_freeContext(pContext);

    pFrame->pts = pts;
}

这里主要就是用 sws_scale 转换图像格式。

Frame 转换成 Packet,Packet 写入文件

bool VideoRecorder::writeFrame(const AVFrame *pFrame)
{
    //qDebug() << "IN VideoRecorder::writeFrame";
    m_errorcode = avcodec_send_frame(m_pCodecCtx, pFrame);
    if(m_errorcode < 0)
    {
        qWarning() << "in VideoRecorder::writeFrame avcodec_send_frame failed";
        return false;
    }

    while (m_errorcode >= 0)
    {
        m_errorcode = avcodec_receive_packet(m_pCodecCtx, m_pPacket);
        if (m_errorcode == AVERROR(EAGAIN) || m_errorcode == AVERROR_EOF)
        {
            return true;
        }
        else if (m_errorcode < 0)
        {
            qWarning() << "in VideoRecorder::writeFrame avcodec_receive_packet failed";
            return false;
        }
        m_pPacket->stream_index = 0;
        AVRational out_timebase = m_pFormatCtx->streams[0]->time_base;

        m_pPacket->pts = av_rescale_q_rnd(m_pPacket->pts, m_time_base, out_timebase, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        m_pPacket->dts = av_rescale_q_rnd(m_pPacket->dts, m_time_base, out_timebase,  (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        m_pPacket->duration = av_rescale_q(m_pPacket->duration, m_time_base, out_timebase);
        m_pPacket->pos = -1;

        m_errorcode = av_interleaved_write_frame(m_pFormatCtx, m_pPacket);
        if (m_errorcode < 0)
        {
            qWarning() << "in VideoRecorder::writeFrame av_interleaved_write_frame failed";
            return false;
        }
        av_packet_unref(m_pPacket);
    }
    return true;
}

setImage 函数

简单的说这个函数就是把 QImage 转成 AVFrame 然后再转成 AVPacket 存入文件。但是 转成 AVFrame 时需要确定 pts。这个函数会获取系统时间,来计算当前 QImage 对应的 pts.

如果是第一幅图像,还要初始化Codec 等工作。

bool VideoRecorder::setImage(const QImage &image, int pts)
{
    //qDebug() << "IN VideoRecorder::setImage";
    if(!m_recording)
    {
        qDebug() << "in VideoRecorder::setImage m_recording = false";
        return false;
    }
    QTime t = QTime::currentTime();
    if( pts < 0 ) // 说明这时要用真实的时间来做为 pts
    {
        if(m_startTime.isNull())
        {
            m_startTime = t; // 说明这是第一帧。需要初始化起始时间。
        }
        int oldpts = m_startTime.msecsTo(t);

        pts = av_rescale_q_rnd(oldpts, AVRational({1, 1000}), m_time_base, AV_ROUND_NEAR_INF);
        //qDebug() << "oldpts = " << oldpts << ", pts = " << pts;
    }
    //qDebug() << "pts = " << pts;
    if(m_width == 0) // 说明这是第一个帧
    {
        initFile(m_codecID, image.size());
        writeHeader();
        av_dump_format(m_pFormatCtx, 0, m_url.toLocal8Bit().constData(), true);
    }
    buildFrameFromImage(m_pFrame, image, pts);
    return writeFrame(m_pFrame);
}

视频文件结尾处理

这里要特别解释一下。 close 函数中有这么一句:writeFrame(nullptr)

这句的作用是将Codec 中缓存的 Packet 都写到文件中。保证我们输入的所有图像都能保存进视频文件中。

int VideoRecorder::writeTrailer()
{
    m_errorcode = av_write_trailer(m_pFormatCtx);
    if(m_errorcode < 0)
    {
        qWarning() << "in VideoRecorder::writeTrailer av_write_trailer failed";
        return -1;
    }
    return 0;
}
bool VideoRecorder::close()
{
    m_recording = false;
    writeFrame(nullptr);
    writeTrailer();
    if (m_pFormatCtx && !(m_pFormatCtx->flags & AVFMT_NOFILE))
    {
        m_errorcode = avio_closep(&m_pFormatCtx->pb);
    }

    m_width = 0;
    m_height = 0;
    return true;
}

其他杂项

AVFrame 里的图像应该用什么格式。这个在 setAVCodecID 函数中会检验一下。如果当前 Codec 不支持这个格式,我们代码会自动选一个支持的图像格式。

void VideoRecorder::setAVCodecID(AVCodecID id)
{
    m_codecID = id;
    m_pCodec = avcodec_find_encoder(id);
    if(m_pCodec)
    {
        const enum AVPixelFormat * pFormat = m_pCodec->pix_fmts;
        if(pFormat)
        {
            while (*pFormat != AV_PIX_FMT_NONE)
            {
                if(*pFormat == m_format)
                {
                    return;
                }
                pFormat ++;
            }
            // 到这里说明 m_format 不在当前 codec 支持的 format 里
            pFormat = m_pCodec->pix_fmts;
            m_format = *pFormat; // 默认使用 codec 支持的第一个 format
        }
  

至此,这个类就基本介绍完成了。下面是头文件

#ifndef VIDEORECORDER_H
#define VIDEORECORDER_H

#include <QObject>
#include <QTime>
#include <QTimer>
#include <QSize>
#include <QImage>
#include <QQueue>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
}

namespace Qly {

class VideoRecorder : public QObject
{
    Q_OBJECT
public:
    explicit VideoRecorder(QObject *parent = nullptr);
    ~VideoRecorder();
    /**
     * @brief setAVCodecID 设置编码类型。默认是 MPEG4
     * @param id
     */
    void setAVCodecID(AVCodecID id);

    /**
     * @brief setTimeBase 设置视频文件的time base, 默认是 1/1000。 也就是 1ms 为基本单位。
     * @param timebase
     */
    void setTimeBase(AVRational timebase) {m_time_base = timebase;}

    /**
     * @brief openFile 建立视频文件
     * @param url
     * @return
     */
    int openFile(QString url);

    /**
     * @brief setImage 将图像插入到视频中
     * @param image
     * @param pts 时间戳,以 time base 为基本单位。第一张图像默认 pts 为 0。 如果 pts = -1 则根据当前时间自动计算 pts.
     * @return
     */
    bool setImage(const QImage &image, int pts);

    /**
    * @brief close 关闭视频文件
    * @return
    */
    bool close();
//public slots:

    /**
     * @brief setImage 将图像插入到视频中,以当前时间自动计算 pts
     * @param image
     * @return
     */
    bool setImage(const QImage &image);

    int errorcode() const {return m_errorcode;}

protected:
    int writeHeader();
    int writeTrailer();
    bool writeFrame(const AVFrame *m_pFrame);
    int initFile(AVCodecID codecID, QSize size);
    void initStreamParameters(AVStream *stream);
    void buildFrameFromImage(AVFrame *m_pFrame, const QImage &image, int pts);

    AVFormatContext *m_pFormatCtx = nullptr;
    const AVCodec *m_pCodec = nullptr;
    AVCodecContext *m_pCodecCtx = nullptr;
    AVFrame *m_pFrame = nullptr;
    AVPacket *m_pPacket = nullptr;

    AVCodecID m_codecID = AV_CODEC_ID_MPEG4;
    AVPixelFormat m_format = AV_PIX_FMT_YUV420P;
    AVRational m_time_base = {1, 1000};

    int64_t m_bit_rate = 10000000;
    int m_width = 0;
    int m_height = 0;
    QString m_url;
private:
    int m_errorcode = 0;
    bool m_recording = false;
    QTime m_startTime;
    QTimer m_timer;
};

}
#endif // VIDEORECORDER_H

TODO 接下来的工作

  1. 我们知道视频录制很占硬盘空间。所以应该设置一个时间限,超过这个时间就自动停。这个功能可以用一个 QTimer 来实现。我的代码里已经加入这个 QTimer 了。但是还没时间来完善这块代码。
  2. 这个代码里还可以加入声音录制的功能。后面有空了也会加进去。

后记,记一个小 BUG 引起的大问题

写这个程序的时候还有些曲折,测试时程序经常莫名其妙的挂掉。开始时没什么方向,一直认为是我代码的逻辑有错。在网上找了许多文章,对照着找问题,都没找到。后来又觉得会不会是编译的 ffmpeg 有问题,又重新编译了一遍,还是不行。最后还是用了 print 大法发现了问题点。这个 bug 浪费了我一整天时间。

下面这个代码片段会概率性的导致程序崩溃。

if(m_pFormatCtx)
{
	avformat_free_context(m_pFormatCtx);
}

一检查才发现 m_pFormatCtx 忘记初始化为 nullptr 了,导致 avformat_free_context() 引发程序崩溃。

也给大家提个醒。

  1. 指针一定要初始化为 nullptr。
  2. 99.9999% 的 bug 都是自己代码的问题。不要轻易的怀疑人家的库有问题。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值