本文参考了雷博士的博客:
最简单的基于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 这个类。我们所有的视频编码相关的代码都封装在里面了。
视频编码大体可以分成这么几步:
打开视频文件,做必要的准备
图像写入 Frame
Frame 转换成 Packet
Packet 写入文件中
下面也分这么几步来介绍。
首先在建立这个类的时候要初始化几个指针。
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; }
这部分比较简单。我就实现了一个功能,把 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 转换图像格式。
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; }
简单的说这个函数就是把 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