本篇文章的需求是将相机获取到的图片进行编码,编码成一个视频,耗费了大约一个星期的时间在解决各种问题。这里阐述一下这篇文章所要解决的几个问题:
1、如何将多张图片编码成视频。
2、如何进行定时录制视频。
3、同时开启多线程进行视频录制。
4、对录制文件目录进行管理:每次都检测录制目录大小是否超过指定大小,如果超过,则删除指定大小的时间最早的一些文件。
1、下载链接: https://ffmpeg.org/download.html
2、
3、
4、由于我是在Win10下,所以选择:
Win10 + Qt8.0.2(MSVC2019) + FFmpeg 4.4
上面的流程图,基本上就是这次这个项目的整体流程了,上面的 2 3 4点都是在单例层完成的。只有第一点是在FFmpegRecord那一层完成的。
首先,确定目标,我是要“同时录制多个视频”,所以,必须得开多个线程,并且,在这一层,就必须要完成:
1、给上层应用的调用提供一个接口。
2、进行定时关闭录制的操作。
3、对底层FFmpegRecord对象进行管理。
4、对录制文件进行管理——在开启录制的时候,检测当前目录文件的大小。
先给出开录制与关录制中整体的代码,然后再慢慢进行解释吧.
//开启录制的接口
QString CRecordMgr::StartRecord(eRecordType eType, QString sFileName, int iRecordSTime)
{
QMutexLocker oLocker(&m_mutex);
QString sPath = "/ics/recordfile";
quint64 iCountByte = _DetectDiskInfo(sPath);
if (iCountByte > DetectMinMB*1024*1024)
{
_RemoveRecordFile(sPath);
QThread::msleep(1000);
}
if (sFileName.isEmpty())
{
qint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
sFileName = QString("%1.mp4").arg(timestamp);
}
_PreActionStart();
QString sMissionId = CCommonFunc::CreateUUID();
CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();
if (pFFmpegRecord)
{
pFFmpegRecord->setObjectName(sMissionId);
m_mapFFmpegRecord.insert(sMissionId, pFFmpegRecord);
m_mapRecordFileName.insert(sMissionId, sFileName);
m_mapRecordType.insert(sMissionId, eType);
pFFmpegRecord->SetMissionId(sMissionId);
pFFmpegRecord->SetRecordType((CFFmpegRecord::eRecordType)eType);
pFFmpegRecord->SetRecordFileName(sFileName);
pFFmpegRecord->Init();
int iTimeId = startTimer((iRecordSTime+1)*1000);//多录制1s,防止最后一帧未录制完整
m_mapTimerId.insert(sMissionId, iTimeId);
m_oConnect = connect(gVideoMgr::instance(), &CVideoMgr::SIGNAL_CommonCameraImage, pFFmpegRecord, &CFFmpegRecord::SLOT_FFmpegImage, Qt::QueuedConnection);
m_mapConnect.insert(sMissionId, m_oConnect);
}
return sMissionId;
}
//使用任务Id对录制任务进行关闭
bool CRecordMgr::StopRecord(CRecordMgr::eRecordType eType)
{
m_setDeleteId.clear();
QMap::iterator it;
for (it = m_mapRecordType.begin(); it != m_mapRecordType.end(); ++it)
{
QString sId = it.key();
CRecordMgr::eRecordType eRecordType = it.value();
if (eType == eRecordType)
{
CFFmpegRecord *pFFmpegRecord = m_mapFFmpegRecord.value(sId);
if (pFFmpegRecord)
{
pFFmpegRecord->StopRecord();
QEventLoop eventloop;
QTimer::singleShot(500, &eventloop, SLOT(quit()));
eventloop.exec();
QMetaObject::Connection oConnect = m_mapConnect.value(sId);
disconnect(oConnect);
CRecordMgr::eRecordType eRecordType = m_mapRecordType.value(sId);
QString sFileName = m_mapRecordFileName.value(sId);
int iTimerId = m_mapTimerId.value(sId);
killTimer(iTimerId);
if (m_mapFFmpegRecord.size() == 0)
{
_PreActionEnd();
}
pFFmpegRecord->deleteLater();
pFFmpegRecord = nullptr;
emit SIGNAL_RecordTask_Finished(eRecordType, sId, sFileName);
m_mapFFmpegRecord.remove(sId);
m_setDeleteId.insert(sId);
m_mapRecordFileName.remove(sId);
m_mapTimerId.remove(sId);
}
}
}
for (auto sId : m_setDeleteId)
{
m_mapRecordType.remove(sId);
}
return true;
}
基本上,就是直接调用就可以了,由于我这边是单例,所以我直接调用接口就可以了,如果你们没有用单例,就看你们怎么设计了。
gRecordMgr::instance->StartRecord(eAlarmRecord, "");
gRecordMgr::instance->StopRecord();
定时录制,最开始是在最底层去操作的,也就是在FFmpegRecord层去操作的,结果至少花费了我一天多的时间在找出问题的所在,明明定时器的设置,定时器的触发都很简单,但就是没办法触发。后面才发觉,原来是最底层的FFmpegRecord还需要去接收外部的图片,基本资源都一直被占用着,所以,根本轮不到定时器的触发,这就很糟糕,所以,最后觉得在RecordMgr层去做定时器的操作,才得以完成。 使用了两种方式,可以使用QTimer, 也可以使用timeEvent,startTimer;其实可能使用startTimer来触发,是更合适的,因为是要有多个定时器的。
方法一
m_pRecordTimer = new QTimer(this);
m_pRecordTimer->setSingleShot(true);
m_pRecordTimer->setObjectName(sMissionId);
connect(m_pRecordTimer, &QTimer::timeout, this, &CRecordMgr::SLOT_StopRecord);//超时了就调用对应的槽函数
m_mapTimer.insert(sMissionId, m_pRecordTimer);
QtConcurrent::run([this,iRecordSTime](){
if (m_pRecordTimer)
{
QMetaObject::invokeMethod(m_pRecordTimer, "start", Qt::QueuedConnection,
Q_ARG(int, iRecordSTime*1000));
}
});
方法二
int iTimerId = startTimer(m_pRecordTimer);
void timeEvent(QTimeEvent *event);
void CRecordMgr::timerEvent(QTimerEvent *timerEvent)
{
int iTimerId = timerEvent->timerId();
QString sMissionId = m_mapTimerId.key(iTimerId);
StopRecord(sMissionId);
}
CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();//创建对象
if (pFFmpegRecord)
{
pFFmpegRecord->setObjectName(sMissionId);//设置对象名称
m_mapFFmpegRecord.insert(sMissionId, pFFmpegRecord);//将这个对象,使用QMap进行存储
}
调用方式:
QString sPath = "/myPath/recordfile";
quint64 iCountByte = _DetectDiskInfo(sPath);//检测某文件路径下的所有文件字节数
//当这个字节数大于某个指定的值得时候
if (iCountByte > DetectMinMB*1024*1024)
{
_RemoveRecordFile(sPath);//移除时间最早的500MB的文件
QThread::msleep(1000);
}
具体的函数:
quint64 CRecordMgr::_DetectDiskInfo(QString _sPath)
{
QDir dir(_sPath);
quint64 size = 0;
foreach(QFileInfo fileInfo, dir.entryInfoList(QDir::Files))
{
//计算文件大小
size += fileInfo.size();
}
foreach(QString subDir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
{
//若存在子目录,则递归调用dirFileSize()函数
size += _DetectDiskInfo(_sPath + QDir::separator() + subDir);
}
return size;
}
void CRecordMgr::_RemoveRecordFile(QString _sPath)
{
QDir dir(_sPath);
if (!dir.exists())
{
return;
}
QStringList lstFileName =_GetFilePath(_sPath);
QList<QString> listPath;
for (auto index : lstFileName)
{
listPath.push_back(index);
}
qSort(listPath.begin(), listPath.end(),[](const QString &str1, const QString &str2){
QStringList lst1 = str1.split("/");
QStringList lst2 = str2.split("/");
QString sComStr1 = lst1.last();
QString sComStr2 = lst2.last();
if (sComStr1.compare(sComStr2)==0)
return sComStr1 < sComStr2;
return sComStr1 < sComStr2;
});
lstFileName.clear();
for (auto index : listPath)
{
lstFileName.append(index);
}
int iFileCount = lstFileName.size();
qint64 iDeleteSize = 0;
const qint64 ciSize = 500 * 1024 * 1024; // 每次删除超过500M即停止删除
for (int i = 0; i < iFileCount; i++)
{
QString sFileName = lstFileName.at(i);
QFileInfo oFileInfo(sFileName);
iDeleteSize += oFileInfo.size();
dir.remove(sFileName);
lstFileName.pop_back();
if (iDeleteSize > ciSize)
{
break;
}
}
}
QStringList CRecordMgr::_GetFilePath(QString _sPath)
{
QStringList listFileInfo;
QDir dir(_sPath);
if (!dir.exists())
{
return listFileInfo;
}
//获取filePath下所有文件夹和文件
dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);//文件夹|文件|不包含./和../
//排序文件夹优先
dir.setSorting(QDir::DirsFirst);
//获取文件夹下所有文件(文件夹+文件)
QFileInfoList list = dir.entryInfoList();
for (int i = 0; i < list.size(); i++)
{
QFileInfo fileInfo = list.at(i);
QStringList listSingleFileInfo;
QString sFilePath = fileInfo.filePath();
if (fileInfo.isDir())//判断是否为文件夹
{
listSingleFileInfo = _GetFilePath(fileInfo.filePath());//递归开始
listFileInfo.append(listSingleFileInfo);
}
else
{
listFileInfo.append(sFilePath);
}
}
return listFileInfo;
}
这部分的内容,基本上移植是比较方便的,注意使用环境是在Arm上的,未尝试过在windows环境进行操作。
老规矩,先把整体的代码贴出来,后面再详细解释一下。
CFFmpegRecord::CFFmpegRecord(QObject *parent) : QObject(parent)
{
}
CFFmpegRecord::~CFFmpegRecord()
{
LOG_INFO << "--> ~CFFmpegRecord Start";
av_free(m_pBuffer);
av_free(m_pYuvBuffer);
sws_freeContext(m_SwsImgCtx);
if (m_pOutPutFormatCtx)
{
avio_close(m_pOutPutFormatCtx->pb);
avformat_free_context(m_pOutPutFormatCtx);
}
if (m_pEncodecCtx)
avcodec_free_context(&m_pEncodecCtx);
if (m_pRgbFrame)
av_frame_free(&m_pRgbFrame);
if (m_pYuvFrame)
av_frame_free(&m_pYuvFrame);
m_pYuvBuffer = nullptr;
m_pBuffer = nullptr;
m_SwsImgCtx = nullptr;
m_pRgbFrame = nullptr;
m_pYuvFrame = nullptr;
m_pOutPutFormatCtx = nullptr;
m_pEncodecCtx = nullptr;
if (m_pThread)
{
m_pThread->quit();
m_pThread->exit();
}
LOG_INFO << "--> ~CFFmpegRecord End";
}
void CFFmpegRecord::Init()
{
m_pThread = new QThread();
m_pThread->setObjectName("CFFmpegRecord");
this->moveToThread(m_pThread);
m_pThread->start();
m_bExitThread = false;
}
void CFFmpegRecord::InitFFmpeg()
{
int ret = 0;
m_iPerFrameCnt = 10;
m_iIndex = 0;
m_pEnPacket = av_packet_alloc();
string sRecordName = m_sRecordFileName.toStdString();
m_cRecordFileName = sRecordName.c_str();
ret = avformat_alloc_output_context2(&m_pOutPutFormatCtx, NULL, NULL, m_cRecordFileName);
if (ret < 0)
{
LOG_INFO << "Cannot alloc output file context";
return;
}
ret = avio_open(&m_pOutPutFormatCtx->pb, m_cRecordFileName, AVIO_FLAG_READ_WRITE);
if (ret < 0)
{
LOG_INFO << "output file open failed";
return;
}
pEncodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (pEncodec == NULL)
{
LOG_INFO << "Cannot find any endcoder";
return;
}
m_pEncodecCtx = avcodec_alloc_context3(pEncodec);
if (m_pEncodecCtx == NULL)
{
LOG_INFO << "Cannot alloc AVCodecContext";
return;
}
m_pVideoStream = avformat_new_stream(m_pOutPutFormatCtx, pEncodec);
if (m_pVideoStream == NULL) {
LOG_INFO << "failed create new video stream";
return;
}
m_pVideoStream->time_base = AVRational{ 1,10 };
m_pCodecParam = m_pVideoStream->codecpar;
m_pCodecParam->width = m_iWidth;
m_pCodecParam->height = m_iHeight;
m_pCodecParam->codec_type = AVMEDIA_TYPE_VIDEO;
ret = avcodec_parameters_to_context(m_pEncodecCtx, m_pCodecParam);
if (ret < 0)
{
LOG_INFO << "Cannot copy codec para";
return;
}
m_pEncodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
m_pEncodecCtx->time_base = AVRational{ 1,10 };
m_pEncodecCtx->bit_rate = 500000;
m_pEncodecCtx->gop_size = 100;
// 某些封装格式必须要设置该标志,否则会造成封装后文件中信息的缺失,如:mp4
if (m_pOutPutFormatCtx->oformat->flags & AVFMT_GLOBALHEADER)
{
m_pEncodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
AVDictionary *param = 0;
if (pEncodec->id == AV_CODEC_ID_H264)
{
m_pEncodecCtx->qmin = 10;
m_pEncodecCtx->qmax = 51;
m_pEncodecCtx->qcompress = (float)0.6;
m_pEncodecCtx->max_b_frames = 0;
av_dict_set(¶m, "preset", "fast", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
}
ret = avcodec_open2(m_pEncodecCtx, pEncodec, ¶m);
if (ret < 0)
{
LOG_INFO << "Open encoder failed";
return;
}
//再将codecCtx设置的参数传给param,用于写入头文件信息
avcodec_parameters_from_context(m_pCodecParam, m_pEncodecCtx);
m_pRgbFrame = av_frame_alloc();
m_pYuvFrame = av_frame_alloc();
m_pYuvFrame->width = m_iWidth;
m_pYuvFrame->height = m_iHeight;
m_pYuvFrame->format = m_pEncodecCtx->pix_fmt;
m_pRgbFrame->width = m_iWidth;
m_pRgbFrame->height = m_iHeight;
m_pRgbFrame->format = AV_PIX_FMT_BGR24;
m_iBufferSize = av_image_get_buffer_size((AVPixelFormat)m_pRgbFrame->format, m_iWidth, m_iHeight, 1);
m_pBuffer = (uint8_t*)av_malloc(m_iBufferSize);
ret = av_image_fill_arrays(m_pRgbFrame->data, m_pRgbFrame->linesize, m_pBuffer, (AVPixelFormat)m_pRgbFrame->format, m_iWidth, m_iHeight, 1);
if (ret < 0)
{
LOG_INFO << "Cannot filled rgbFrame";
return;
}
int yuvSize = av_image_get_buffer_size((AVPixelFormat)m_pYuvFrame->format, m_iWidth, m_iHeight, 1);
m_pYuvBuffer = (uint8_t*)av_malloc(yuvSize);
ret = av_image_fill_arrays(m_pYuvFrame->data, m_pYuvFrame->linesize, m_pYuvBuffer, (AVPixelFormat)m_pYuvFrame->format, m_iWidth, m_iHeight, 1);
if (ret < 0)
{
LOG_INFO << "Cannot filled yuvFrame";
return;
}
m_SwsImgCtx = sws_getContext(m_iWidth, m_iHeight, AV_PIX_FMT_BGR24, m_iWidth, m_iHeight, m_pEncodecCtx->pix_fmt, 0, NULL, NULL, NULL);
ret = avformat_write_header(m_pOutPutFormatCtx, NULL);
if (ret != AVSTREAM_INIT_IN_WRITE_HEADER)
{
LOG_INFO << "Write file header fail";
return;
}
av_new_packet(m_pEnPacket, m_iBufferSize);
}
void CFFmpegRecord::SetMissionId(QString _sMissionId)
{
m_sMissionId = _sMissionId;
}
void CFFmpegRecord::SetRecordType(eRecordType eType)
{
m_iRecordType = eType;
}
void CFFmpegRecord::SetRecordFileName(QString _sFileName)
{
QString sRecordDir;
QDir dir;
switch (m_iRecordType) {
case eAlarmRecord:
{
sRecordDir = QString("/ics/recordfile/alarm/");
if (!dir.exists(sRecordDir))
{
dir.mkpath(sRecordDir);
}
m_sRecordFileName = sRecordDir.append(_sFileName);
}
break;
case eOperateRecord:
{
sRecordDir = QString("/ics/recordfile/operate/");
if (!dir.exists(sRecordDir))
{
dir.mkpath(sRecordDir);
}
m_sRecordFileName = sRecordDir.append(_sFileName);
}
break;
case eTakeRecord:
{
sRecordDir = QString("/ics/recordfile/take/");
if (!dir.exists(sRecordDir))
{
dir.mkpath(sRecordDir);
}
m_sRecordFileName = sRecordDir.append(_sFileName);
}
break;
case eReturnRecord:
{
sRecordDir = QString("/ics/recordfile/return/");
if (!dir.exists(sRecordDir))
{
dir.mkpath(sRecordDir);
}
m_sRecordFileName = sRecordDir.append(_sFileName);
}
break;
default:
break;
}
LOG_INFO << "--> CFFmpegRecord::SetRecordFileName FileName:"<<m_sRecordFileName.toStdString();
}
void CFFmpegRecord::SetRecordTime(int _iRecordTime)
{
m_iRecordTime = _iRecordTime;
}
void CFFmpegRecord::StopRecord()
{
m_bExitThread = true;
}
void CFFmpegRecord::writeImageToMp4(QImage _img)
{
DLOG_TRACE <<"--> CFFmpegRecord::writeImageToMp4 start";
QTime time;
time.start();
if (_img.isNull())
{
LOG_INFO << "--> writeImageToMp4 img is NULL";
return;
}
Mat img = QImage2Mat(_img);
memcpy(m_pBuffer, img.data, m_iBufferSize);
sws_scale(m_SwsImgCtx,
m_pRgbFrame->data,
m_pRgbFrame->linesize,
0,
m_pEncodecCtx->height,
m_pYuvFrame->data,
m_pYuvFrame->linesize);
m_iIndex ++;
m_pYuvFrame->pts = m_iIndex;
rgb2mp4Encode(m_pEncodecCtx, m_pYuvFrame, m_pEnPacket, m_pVideoStream, m_pOutPutFormatCtx);
DLOG_TRACE <<"--> CFFmpegRecord::writeImageToMp4 end"<<time.elapsed();
}
Mat CFFmpegRecord::QImage2Mat(QImage _img)
{
cv::Mat mat;
switch (_img.format())
{
case QImage::Format_ARGB32:
case QImage::Format_RGB32:
case QImage::Format_ARGB32_Premultiplied:
mat = cv::Mat(_img.height(), _img.width(), CV_8UC4, (void*)_img.constBits(), _img.bytesPerLine());
break;
case QImage::Format_RGB888:
mat = cv::Mat(_img.height(), _img.width(), CV_8UC3, (void*)_img.constBits(), _img.bytesPerLine());
cv::cvtColor(mat, mat, CV_BGR2RGB);
break;
case QImage::Format_Indexed8:
mat = cv::Mat(_img.height(), _img.width(), CV_8UC1, (void*)_img.constBits(), _img.bytesPerLine());
break;
}
return mat;
}
int CFFmpegRecord::rgb2mp4Encode(AVCodecContext* codecCtx, AVFrame* yuvFrame, AVPacket* pkt, AVStream* vStream, AVFormatContext* fmtCtx)
{
int ret = 0;
if (avcodec_send_frame(codecCtx, yuvFrame) >= 0)
{
while (avcodec_receive_packet(codecCtx, pkt) >= 0)
{
pkt->stream_index = vStream->index;
pkt->pos = -1;
av_packet_rescale_ts(pkt, codecCtx->time_base, vStream->time_base);
DLOG_TRACE << "encoder success:" << pkt->size << endl;
ret = av_interleaved_write_frame(fmtCtx, pkt);
if (ret < 0)
{
char errStr[256];
av_strerror(ret, errStr, 256);
DLOG_TRACE << "error is:" << errStr << endl;
}
}
}
return ret;
}
void CFFmpegRecord::SLOT_FFmpegImage(const QByteArray &_oYuv420, int _iWidth, int _iHeight)
{
QMutexLocker oLocker(&m_mutex);
DLOG_TRACE << "--> CFFmpegRecord::SLOT_FFmpegImage Start";
QTime time;
time.start();
m_iWidth = _iWidth;
m_iHeight = _iHeight;
if (nullptr == m_pRGBData)
{
m_pRGBData = new uchar[_iWidth * _iHeight * 3];
m_oImage = std::move(QImage(m_pRGBData, _iWidth, _iHeight, QImage::Format_RGB888));
}
CPixelFormatConverter::YUV420ToRGB24((uchar*)_oYuv420.data(), _iWidth, _iHeight, &m_pRGBData);
if (false == m_bInit)
{
InitFFmpeg();
m_bInit = true;
}
if (!m_bExitThread)
{
writeImageToMp4(m_oImage);
}
if (m_bExitThread && m_bExit)
{
rgb2mp4Encode(m_pEncodecCtx, NULL, m_pEnPacket, m_pVideoStream, m_pOutPutFormatCtx);
av_write_trailer(m_pOutPutFormatCtx);
m_bExit = false;
}
LOG_INFO << "--> CFFmpegRecord::SLOT_FFmpegImage End Time is :"<<time.elapsed();
}
这里面最主要的还是这个SLOT_FFmpegImage
这个函数,基本都是围绕这个函数在走所有的流程,包括FFmpeg的初始化,也都在这里面进行初始化了,基本就是一个对象,有一整套所有的东西,然后,录制结束后,再整体释放掉就是。
基本上,这个编码流程还是比较固定的,下面贴一个编码流程。
基本上就是按照这个编码流程进行操作了。
在遍历QMap的时候,将里面的元素remove了,就会导致 “收到进程信号退出”。所以,如果在遍历一个数据结构的时候,就必能在里面直接将元素给remove了,这样会导致程序崩溃。所以,在remove内部元素的时候,要进行关注。
暂时不知道原因,也有可能我使用QTime 进行计时的方式是有问题的。
析构了一个QMutex
参考这个链接:https://www.jianshu.com/p/55b4f187687f
其实主要还是在对象还有信号在传输的过程中,就将对象给释放了,才导致了这样的问题,所以,直接调用delete有可能是存在问题的,要注意,可以使用deleteLater.
这个问题的出现还是很常见的,直接注意一下,基本就是以下的几种问题:
1.内存重复释放,出现double free时,通常是由于这种情况所致。
2.内存泄露,分配的内存忘了释放。
3.内存越界使用,使用了不该使用的内存。
4.使用了无效指针。
5.空指针,对一个空指针进行操作。
https://blog.csdn.net/m0_53601375/article/details/121076916
上面这篇文章算还是可以用的, 我有试过,但要注意其使用的FFmpeg版本,因为FFmpeg的每个版本,确实相差还是很大的,所以,必须要注意,不然,会发现,有很多API都已经不再能使用了。
可以参考这篇文章:https://www.jianshu.com/p/4826c3c34caa
这篇文章也是能完全达到你的目标的,已经亲测过了。
我的整个FFmpeg编码流程基本上也是参考这篇文章的,值得阅读一下。
Qt 有两种使用多线程的方式:
1、继承QThread中的run函数。
2、把继承于QObject的类转移到QThread里 ,也就是使用moveToThread。
我这边使用的就是第二种方式。
接下来,讲讲需要注意的知识点。
1、如果继承于QThread,也就是使用第一种方式的话,则要注意,只有其中的run函数是在新线程中的,其他所有函数都是在主线程的下面。
2、如果是使用moveToThread的话,则是被外部触发的槽函数是在子线程下运行的,其他也都是在主线程下运行。