- 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
- 在这个Demo里主要使用Qt + FFmpeg开发一个摄像头【录像机】,这里主要使用的是【软解码】,需要使用硬解码的可以看之前的文章;
- 在之前的文章中使用了QPainter进行绘制显示,也讲了使用OpenGL显示RGB、YUV图像方式;
- 由于FFmpeg解码得到的像素格式为YUVJ422P,将YUVJ422P转换为RGB或者YUV420p都很麻烦,并且会消耗CPU资源,所以这里直接使用OpenGL显示YUVJ422P图像,(将YUVJ422P转RGB的步骤放到了GPU中进行)。
- 关于打开摄像头部分请看上一章,整理不重复说明,这里主要讲述录像功能。
开发环境说明
系统:Windows10、Ubuntu20.04
Qt版本:V5.12.5
编译器:MSVC2017-64、GCC/G++64
FFmpeg版本:n5.1.2 (注意:如果版本不对可能程序无法运行) |
- 使用ffmpeg音视频库【软解码】打开本地摄像头【录制视频】保存到本地;
- 采用【OpenGL显示YUV】图像,支持自适应窗口缩放,支持使用QOpenGLWidget、QOpenGLWindow显示;
- 将YUV转RGB的步骤由CPU转换改为使用GPU转换,降低CPU占用率;
- 支持Windows、Linux打开本地摄像头;
- 支持使用【静态帧率】、【动态帧率】录制视频;
- 视频解码、线程控制、显示各部分功能分离,低耦合度。
- 采用最新的5.1.2版本ffmpeg库进行开发,超详细注释信息,将所有踩过的坑、解决办法、注意事项都得很写清楚。
啥也不说了,直接上代码,一切有注释
videosave.h文件
/******************************************************************************
* @文件名 videosave.h
* @功能 将视频编码后保存到文件中
*
* @开发者 mhf
* @邮箱 1603291350@qq.com
* @时间 2022/11/29
* @备注
*****************************************************************************/
#ifndef VIDEOSAVE_H
#define VIDEOSAVE_H
#include
#include
struct AVCodecParameters;
struct AVFormatContext;
struct AVCodecContext;
struct AVStream;
struct AVFrame;
struct AVPacket;
struct AVOutputFormat;
class VideoSave
{
public:
VideoSave();
~VideoSave();
bool open(AVStream *inStream, const QString& fileName);
void write(AVFrame* frame);
void close();
private:
void showError(int err);
private:
AVFormatContext* m_formatContext = nullptr;
AVCodecContext * m_codecContext = nullptr; // 编码器上下文
AVStream * m_videoStream = nullptr;
AVPacket * m_packet = nullptr; // 数据包
int m_index = 0;
bool m_writeHeader = false; // 是否写入头
QMutex m_mutex;
};
#endif // VIDEOSAVE_H
videosave.cpp文件
#include "videosave.h"
#include
extern "C" { // 用C规则编译指定的代码
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavdevice/avdevice.h"
}
#define ERROR_LEN 1024 // 异常信息数组长度
#define PRINT_LOG 1
#define USE_H264 0 // 使用H264编码器
VideoSave::VideoSave()
{
}
VideoSave::~VideoSave()
{
close();
}
/**
* @brief 显示ffmpeg函数调用异常信息
* @param err
*/
void VideoSave::showError(int err)
{
#if PRINT_LOG
static char m_error[ERROR_LEN]; // 保存异常信息
memset(m_error, 0, ERROR_LEN); // 将数组置零
av_strerror(err, m_error, ERROR_LEN);
qWarning() << "VideoSave Error:" << m_error;
#else
Q_UNUSED(err)
#endif
}
bool VideoSave::open(AVStream *inStream, const QString &fileName)
{
if(!inStream || fileName.isEmpty()) return false;
// 通过输出文件名为输出格式分配AVFormatContext。
#if USE_H264
int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "h264", fileName.toStdString().data());
#else
/**
* 摄像头打开使用的是mjpeg编码器;
* MJPEG压缩技术可以获取清晰度很高的视频图像,可以【动态调整帧率】适合保存摄像头视频、分辨率。但由于没有考虑到帧间变化,造成大量冗余信息被重复存储,因此单帧视频的占用空间较大;
* 如果采用其它编码器,由于摄像头曝光时间长度不一定,所以录像时帧率一直在变,编码器指定固定帧率会导致视频一会快一会慢,效果很不好,适用于录制固定帧率的视频(当然其它编码器应该是有处理办法,不过我还不清楚);
*/
QString strName = avcodec_find_encoder(inStream->codecpar->codec_id)->name; // 获取编码器名称
int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, strName.toStdString().data(), fileName.toStdString().data()); // 这里使用和解码一样的编码器,防止保存的图像颜色出问题
#endif
if(ret < 0)
{
close();
showError(ret);
return false;
}
// 创建并初始化AVIOContext以访问url所指示的资源。
ret = avio_open(&m_formatContext->pb, fileName.toStdString().data(), AVIO_FLAG_WRITE);
if(ret < 0)
{
close();
showError(ret);
return false;
}
// 查询编码器
const AVCodec* codec = avcodec_find_encoder(m_formatContext->oformat->video_codec);
if(!codec)
{
close();
showError(AVERROR(ENOMEM));
return false;
}
// 分配AVCodecContext并将其字段设置为默认值。
m_codecContext = avcodec_alloc_context3(codec);
if(!m_codecContext)
{
close();
showError(AVERROR(ENOMEM));
return false;
}
// 设置编码器上下文参数
m_codecContext->width = inStream->codecpar->width; // 图片宽度/高度
m_codecContext->height = inStream->codecpar->height;
#if USE_H264
m_codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
#else
m_codecContext->pix_fmt = AVPixelFormat(inStream->codecpar->format); // 像素格式,也可以使用codec->pix_fmts[0]或AV_PIX_FMT_YUVJ422P(【注意】摄像头解码的图像格式为yuvj422p,如果这里不一样可能保存会出问题,或者后面进行格式转换)
#endif
m_codecContext->time_base = {1, 10}; //设置时间基,20为分母,1为分子,表示以1/20秒时间间隔播放一帧图像
m_codecContext->framerate = {10, 1};
m_codecContext->bit_rate = 4000000; // 目标的码率,即采样的码率;显然,采样码率越大,视频大小越大,画质越高
m_codecContext->gop_size = 10; // I帧间隔
m_codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
// m_codecContext->max_b_frames = 1; // 非B帧之间的最大B帧数(有些格式不支持)
// m_codecContext->qmin = 1;
// m_codecContext->qmax = 5;
// m_codecContext->colorspace = AVCOL_SPC_BT470BG;
// m_codecContext->color_range = AVCOL_RANGE_JPEG;
// m_codecContext->color_primaries = AVCOL_PRI_BT709;
// m_codecContext->bits_per_coded_sample = 24;
// m_codecContext->bits_per_raw_sample = 8;
// av_opt_set(m_codecContext->priv_data, "preset", "placebo", 0);
// qDebug() << m_codecContext->pix_fmt;
// 打开编码器
ret = avcodec_open2(m_codecContext, nullptr, nullptr);
#if USE_H264
ret = avcodec_open2(m_codecContext, codec, nullptr); // 使用h264时第一次打不开,第二次可以打卡,不知道什么原因
#endif
if(ret < 0)
{
close();
showError(ret);
return false;
}
// 向媒体文件添加新流
m_videoStream = avformat_new_stream(m_formatContext, nullptr);
if(!m_videoStream)
{
close();
showError(AVERROR(ENOMEM));
return false;
}
//拷贝一些参数,给codecpar赋值
ret = avcodec_parameters_from_context(m_videoStream->codecpar,m_codecContext);
if(ret < 0)
{
close();
showError(ret);
return false;
}
// 写入文件头
ret = avformat_write_header(m_formatContext, nullptr);
if(ret < 0)
{
close();
showError(ret);
return false;
}
m_writeHeader = true;
// 分配一个AVPacket
m_packet = av_packet_alloc();
if(!m_packet)
{
close();
showError(AVERROR(ENOMEM));
return false;
}
qDebug() << "开始录制视频!";
return true;
}
/**
* @brief 写入数据
* @param frame
*/
void VideoSave::write(AVFrame *frame)
{
QMutexLocker locker(&m_mutex);
if(!m_packet)
{
return;
}
if(frame)
{
frame->pts = m_index; // 注意:每一帧视频显示时间从0递增,否则录制的视频显示/时长会不对
m_index++;
}
// 将图像传入编码器
avcodec_send_frame(m_codecContext, frame);
// 循环读取所有编码完的帧
while (true)
{
// 从编码器中读取图像帧
int ret = avcodec_receive_packet(m_codecContext, m_packet);
if(ret < 0)
{
break;
}
// 将数据包中的有效时间字段(时间戳/持续时间)从一个时基转换为 输出流的时间
av_packet_rescale_ts(m_packet, m_codecContext->time_base, m_videoStream->time_base);
av_write_frame(m_formatContext, m_packet); // 将数据包写入输出媒体文件
av_packet_unref(m_packet);
}
}
/**
* @brief 关闭保存数据
*/
void VideoSave::close()
{
write(nullptr); // 传入空帧,读取所有编码数据
QMutexLocker locker(&m_mutex); // 如果不加锁可能在点击关闭时,write函数正在写入数据,导致崩溃
if(m_formatContext)
{
// 写入文件尾
if(m_writeHeader)
{
m_writeHeader = false;
int ret = av_write_trailer(m_formatContext);
if(ret < 0)
{
showError(ret);
return;
}
}
int ret = avio_close(m_formatContext->pb);
if(ret < 0)
{
showError(ret);
return;
}
avformat_free_context(m_formatContext);
m_formatContext = nullptr;
}
// 释放编解码器上下文并置空
if(m_codecContext)
{
avcodec_free_context(&m_codecContext);
}
if(m_packet)
{
av_packet_free(&m_packet);
qDebug() << "停止录制视频!";
}
}