本篇文章的需求是將相機(jī)獲取到的圖片進(jìn)行編碼,編碼成一個(gè)視頻,耗費(fèi)了大約一個(gè)星期的時(shí)間在解決各種問題。這里闡述一下這篇文章所要解決的幾個(gè)問題:
成都網(wǎng)絡(luò)公司-成都網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián)建站十多年經(jīng)驗(yàn)成就非凡,專業(yè)從事網(wǎng)站建設(shè)、成都做網(wǎng)站,成都網(wǎng)頁(yè)設(shè)計(jì),成都網(wǎng)頁(yè)制作,軟文發(fā)稿,廣告投放平臺(tái)等。十多年來(lái)已成功提供全面的成都網(wǎng)站建設(shè)方案,打造行業(yè)特色的成都網(wǎng)站建設(shè)案例,建站熱線:18980820575,我們期待您的來(lái)電!正文 一、準(zhǔn)備工作 1、下載FFmpeg的開發(fā)版1、如何將多張圖片編碼成視頻。
2、如何進(jìn)行定時(shí)錄制視頻。
3、同時(shí)開啟多線程進(jìn)行視頻錄制。
4、對(duì)錄制文件目錄進(jìn)行管理:每次都檢測(cè)錄制目錄大小是否超過(guò)指定大小,如果超過(guò),則刪除指定大小的時(shí)間最早的一些文件。
1、下載鏈接: https://ffmpeg.org/download.html
2、
3、
4、由于我是在Win10下,所以選擇:
2、使用環(huán)境Win10 + Qt8.0.2(MSVC2019) + FFmpeg 4.4
二、整體流程解析上面的流程圖,基本上就是這次這個(gè)項(xiàng)目的整體流程了,上面的 2 3 4點(diǎn)都是在單例層完成的。只有第一點(diǎn)是在FFmpegRecord那一層完成的。
三、單例模式——多線程視頻錄制首先,確定目標(biāo),我是要“同時(shí)錄制多個(gè)視頻”,所以,必須得開多個(gè)線程,并且,在這一層,就必須要完成:
1、給上層應(yīng)用的調(diào)用提供一個(gè)接口。
2、進(jìn)行定時(shí)關(guān)閉錄制的操作。
3、對(duì)底層FFmpegRecord對(duì)象進(jìn)行管理。
4、對(duì)錄制文件進(jìn)行管理——在開啟錄制的時(shí)候,檢測(cè)當(dāng)前目錄文件的大小。
先給出開錄制與關(guān)錄制中整體的代碼,然后再慢慢進(jìn)行解釋吧.
//開啟錄制的接口
QString CRecordMgr::StartRecord(eRecordType eType, QString sFileName, int iRecordSTime)
{QMutexLocker oLocker(&m_mutex);
QString sPath = "/myPath/recordfile";
quint64 iCountByte = _DetectDiskInfo(sPath);//檢測(cè)某文件路徑下的所有文件字節(jié)數(shù)
//當(dāng)這個(gè)字節(jié)數(shù)大于某個(gè)指定的值得時(shí)候
if (iCountByte >DetectMinMB*1024*1024)
{_RemoveRecordFile(sPath);//移除時(shí)間最早的500MB的文件
QThread::msleep(1000);
}
if (sFileName.isEmpty())
{qint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
sFileName = QString("%1.mp4").arg(timestamp);
}
_PreActionStart();//開啟要傳圖過(guò)來(lái)的相機(jī)
QString sMissionId = CCommonFunc::CreateUUID();
CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();//創(chuàng)建對(duì)象
if (pFFmpegRecord)
{pFFmpegRecord->setObjectName(sMissionId);//設(shè)置對(duì)象名詞
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->SetRecordTime(iRecordSTime);
pFFmpegRecord->Init();
//為這個(gè)對(duì)象創(chuàng)建一個(gè)定時(shí)器
m_pRecordTimer = new QTimer(this);
m_pRecordTimer->setSingleShot(true);
m_pRecordTimer->setObjectName(sMissionId);
connect(m_pRecordTimer, &QTimer::timeout, this, &CRecordMgr::SLOT_StopRecord);//超時(shí)了就調(diào)用對(duì)應(yīng)的槽函數(shù)
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));
}
});
//將外部傳入的信號(hào)傳入編碼的代碼中
m_oConnect = connect(gVideoMgr::instance(), &CVideoMgr::SIGNAL_CommonCameraImage,pFFmpegRecord, &CFFmpegRecord::SLOT_FFmpegImage, Qt::QueuedConnection);
m_mapConnect.insert(sMissionId, m_oConnect);
}
return sMissionId;
}
//使用任務(wù)Id對(duì)錄制任務(wù)進(jìn)行關(guān)閉
bool CRecordMgr::StopRecord(QString sId)
{
CFFmpegRecord *pFFmpegRecord = m_mapFFmpegRecord.value(sId);
if (pFFmpegRecord)
{
pFFmpegRecord->StopRecord();//對(duì)底下的錄制任務(wù)進(jìn)行關(guān)閉
QThread::msleep(500);
QMetaObject::Connection oConnect = m_mapConnect.value(sId);
disconnect(oConnect);
CRecordMgr::eRecordType eRecordType = m_mapRecordType.value(sId);
QString sFileName = m_mapRecordFileName.value(sId);
QTimer *pTimer = m_mapTimer.value(sId);
if (pTimer && pTimer->isActive())
pTimer->stop();
if (m_mapFFmpegRecord.size() == 0)
{
_PreActionEnd();//當(dāng)沒有任務(wù)錄制任務(wù)的時(shí)候,關(guān)閉相機(jī)
}
pFFmpegRecord->deleteLater();//最好使用deletelater() 直接刪除,可能此時(shí)某些緩存的圖片還沒傳輸結(jié)束
pFFmpegRecord = nullptr;
emit SIGNAL_RecordTask_Finished(eRecordType, sId, sFileName);//給外部提供錄制結(jié)束的接口
m_mapFFmpegRecord.remove(sId);
m_mapRecordType.remove(sId);
m_mapRecordFileName.remove(sId);
m_mapTimer.remove(sId);
}
return true;
}
1、給上層應(yīng)用一個(gè)開啟錄制與關(guān)閉錄制的接口基本上,就是直接調(diào)用就可以了,由于我這邊是單例,所以我直接調(diào)用接口就可以了,如果你們沒有用單例,就看你們?cè)趺丛O(shè)計(jì)了。
gRecordMgr::instance->StartRecord(eAlarmRecord, "");
gRecordMgr::instance->StopRecord();
2、定時(shí)錄制的功能定時(shí)錄制,最開始是在最底層去操作的,也就是在FFmpegRecord層去操作的,結(jié)果至少花費(fèi)了我一天多的時(shí)間在找出問題的所在,明明定時(shí)器的設(shè)置,定時(shí)器的觸發(fā)都很簡(jiǎn)單,但就是沒辦法觸發(fā)。后面才發(fā)覺,原來(lái)是最底層的FFmpegRecord還需要去接收外部的圖片,基本資源都一直被占用著,所以,根本輪不到定時(shí)器的觸發(fā),這就很糟糕,所以,最后覺得在RecordMgr層去做定時(shí)器的操作,才得以完成。 使用了兩種方式,可以使用QTimer, 也可以使用timeEvent,startTimer;其實(shí)可能使用startTimer來(lái)觸發(fā),是更合適的,因?yàn)槭且卸鄠€(gè)定時(shí)器的。
方法一
m_pRecordTimer = new QTimer(this);
m_pRecordTimer->setSingleShot(true);
m_pRecordTimer->setObjectName(sMissionId);
connect(m_pRecordTimer, &QTimer::timeout, this, &CRecordMgr::SLOT_StopRecord);//超時(shí)了就調(diào)用對(duì)應(yīng)的槽函數(shù)
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);
3、對(duì)錄制對(duì)象進(jìn)行管理CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();//創(chuàng)建對(duì)象
if (pFFmpegRecord)
{pFFmpegRecord->setObjectName(sMissionId);//設(shè)置對(duì)象名稱
m_mapFFmpegRecord.insert(sMissionId, pFFmpegRecord);//將這個(gè)對(duì)象,使用QMap進(jìn)行存儲(chǔ)
}
4、對(duì)錄制文件進(jìn)行管理調(diào)用方式:
QString sPath = "/myPath/recordfile";
quint64 iCountByte = _DetectDiskInfo(sPath);//檢測(cè)某文件路徑下的所有文件字節(jié)數(shù)
//當(dāng)這個(gè)字節(jié)數(shù)大于某個(gè)指定的值得時(shí)候
if (iCountByte >DetectMinMB*1024*1024)
{_RemoveRecordFile(sPath);//移除時(shí)間最早的500MB的文件
QThread::msleep(1000);
}
具體的函數(shù):
quint64 CRecordMgr::_DetectDiskInfo(QString _sPath)
{QDir dir(_sPath);
quint64 size = 0;
foreach(QFileInfo fileInfo, dir.entryInfoList(QDir::Files))
{//計(jì)算文件大小
size += fileInfo.size();
}
foreach(QString subDir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
{//若存在子目錄,則遞歸調(diào)用dirFileSize()函數(shù)
size += _DetectDiskInfo(_sPath + QDir::separator() + subDir);
}
return size;
}
void CRecordMgr::_RemoveRecordFile(QString _sPath)
{QDir dir(_sPath);
if (!dir.exists())
{return;
}
QStringList lstFileName =_GetFilePath(_sPath);
QListlistPath;
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; // 每次刪除超過(guò)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);//文件夾|文件|不包含./和../
//排序文件夾優(yōu)先
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;
}
這部分的內(nèi)容,基本上移植是比較方便的,注意使用環(huán)境是在Arm上的,未嘗試過(guò)在windows環(huán)境進(jìn)行操作。
四、FFmpegRecord編碼層老規(guī)矩,先把整體的代碼貼出來(lái),后面再詳細(xì)解釋一下。
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;
// 某些封裝格式必須要設(shè)置該標(biāo)志,否則會(huì)造成封裝后文件中信息的缺失,如: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設(shè)置的參數(shù)傳給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_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"<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 :"<
參考
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級(jí)服務(wù)器適合批量采購(gòu),新人活動(dòng)首月15元起,快前往官網(wǎng)查看詳情吧