1、前言
基本上各種播放器提供的錄制視訊接口,都是隻有開始錄制和結束錄制兩個,當然一般用的最多的也是這兩個接口,但是實際使用過程中,還有一種可能需要中途暫停錄制,暫停以後再次繼續錄制,将中間部分視訊不需要錄制,跳過這部分不需要的視訊,而且錄制的視訊檔案必須是能夠正常播放的連續的。vlc和mpv都隻提供了開始錄制和停止錄制接口,ffmpeg既然是自己解碼,是以錄制完全自己控制,存儲的時候,每一幀的資料都要寫入pts和dts,每次重新計算時間基準保證時間正确,不然不是連續的幀儲存後會跳。
打通了視訊暫停錄制功,還有個應用場景就可以迎難而解,就是多個通道的視訊,不同時段分開存入同一個視訊檔案,類似于将輪詢的過程中展示的視訊挨個存儲到同一個視訊檔案,當然分辨率必須保持一緻,不一緻可能存儲會出問題,這種場景還是比較常見的,比如視訊通道輪詢過程中,指定某個位置的視訊存儲的同一個視訊檔案,将輪詢的整個過程錄制下來,後期以便回放查閱視訊。在目前封裝的元件中,除了打開和關閉錄制,中途隻需要将視訊幀傳入即可,會自動計算換算成正确的pts/dts存儲到MP4檔案中。
2、效果圖
3、體驗位址
- 國内站點:gitee.com/feiyangqing…
- 國際站點:github.com/feiyangqing…
- 個人作品:blog.csdn.net/feiyangqing…
- 體驗位址:pan.baidu.com/s/1d7TH_GEY… 提取碼:01jf 檔案名:bin_video_demo/bin_linux_video。
C++音視訊學習資料免費擷取方法:關注音視訊開發T哥,點選「連結」即可免費擷取2023年最新C++音視訊開發進階獨家免費學習大禮包!
4、相關代碼
void FFmpegThread::recordStart(const QString &fileName)
{
#ifdef videosave
AbstractVideoThread::recordStart(fileName);
if ((saveVideoType > 1) && !onlyAudio) {
this->setFileName(fileName);
//處于暫停階段則切換暫停标志位(暫停後再次恢複說明又重新開始錄制)
if (saveFile->getIsPause()) {
isRecord = true;
saveFile->pause();
emit recorderStateChanged(RecorderState_Recording, fileName);
} else {
saveFile->setPara(saveVideoType, videoWidth, videoHeight, frameRate, formatCtx->streams[videoIndex], (videoType == VideoType_Camera));
saveFile->open(fileName);
if (saveFile->getIsOk()) {
isRecord = true;
emit recorderStateChanged(RecorderState_Recording, fileName);
}
}
}
#endif
}
void FFmpegThread::recordPause()
{
#ifdef videosave
AbstractVideoThread::recordPause();
if ((saveVideoType > 1) && !onlyAudio) {
if (saveFile->getIsOk()) {
isRecord = false;
saveFile->pause();
emit recorderStateChanged(RecorderState_Paused, fileName);
}
}
#endif
}
void FFmpegThread::recordStop()
{
#ifdef videosave
AbstractVideoThread::recordStop();
if ((saveVideoType > 1) && !onlyAudio) {
if (saveFile->getIsOk()) {
isRecord = false;
saveFile->stop();
//執行過轉換合并的不用再發信号
if (!saveFile->isConvertMerge) {
emit recorderStateChanged(RecorderState_Stopped, fileName);
}
}
}
#endif
}
void FFmpegSave::save()
{
//從隊列中取出資料處理
//qDebug() << TIMEMS << videoFrames.count() << videoPackets.count();
if (videoFrames.count() > 0) {
mutex.lock();
AVFrame *frame = videoFrames.takeFirst();
mutex.unlock();
FFmpegHelper::encode(this, videoCodecCtx, videoPacket, frame, true);
FFmpegHelper::freeFrame(frame);
}
if (videoPackets.count() > 0) {
mutex.lock();
AVPacket *packet = videoPackets.takeFirst();
mutex.unlock();
this->writePacket(packet);
FFmpegHelper::freePacket(packet);
}
}
void FFmpegSave::close()
{
//寫入過開始符才能寫入檔案結束符(沒有這個判斷會報錯)
if (packetCount > 0 && saveVideoType == SaveVideoType_Mp4) {
av_write_trailer(formatCtx);
}
//清空隊列中的資料
foreach (AVFrame *frame, videoFrames) {
FFmpegHelper::freeFrame(frame);
}
foreach (AVPacket *packet, videoPackets) {
FFmpegHelper::freePacket(packet);
}
packetCount = 0;
videoFrames.clear();
videoPackets.clear();
//釋放臨時資料包
if (videoPacket) {
FFmpegHelper::freePacket(videoPacket);
videoPacket = NULL;
}
//關閉編碼器上下文并釋放對象
if (videoCodecCtx) {
avcodec_free_context(&videoCodecCtx);
videoCodec = NULL;
videoCodecCtx = NULL;
}
//關閉檔案流并釋放對象
if (formatCtx) {
avio_close(formatCtx->pb);
avformat_free_context(formatCtx);
formatCtx = NULL;
videoStreamOut = NULL;
videoStreamIn = NULL;
}
//執行轉換合并音頻檔案到一個檔案
isConvertMerge = false;
if (convertMerge) {
if (saveVideoType == SaveVideoType_H264) {
isConvertMerge = FFmpegRun::aacAndH264ToMp4(fileName);
} else if (saveVideoType == SaveVideoType_Mp4) {
isConvertMerge = FFmpegRun::aacAndMp4ToMp4(fileName);
}
}
}
void FFmpegSave::setPara(const SaveVideoType &saveVideoType, int videoWidth, int videoHeight, int frameRate, AVStream *videoStreamIn, bool camera)
{
this->saveVideoType = saveVideoType;
this->videoWidth = videoWidth;
this->videoHeight = videoHeight;
if (camera) {
this->frameRate = frameRate > 15 ? 16.621 : frameRate;
} else {
this->frameRate = frameRate > 25 ? 25 : frameRate;
}
this->videoStreamIn = videoStreamIn;
}
void FFmpegSave::writeVideo(AVFrame *frame)
{
//沒打開或者暫停階段不處理
if (!isOk || isPause) {
return;
}
//可以直接寫入到檔案也可以排隊處理
if (directSave) {
FFmpegHelper::encode(this, videoCodecCtx, videoPacket, frame, true);
} else {
mutex.lock();
videoFrames << av_frame_clone(frame);
mutex.unlock();
}
}
void FFmpegSave::writeVideo(AVPacket *packet)
{
//沒打開或者暫停階段不處理
if (!isOk || isPause) {
return;
}
//可以直接寫入到檔案也可以排隊處理
if (directSave) {
this->writePacket(packet);
} else {
mutex.lock();
videoPackets << FFmpegHelper::creatPacket(packet);
mutex.unlock();
}
}
5、功能特點
5.1 基礎功能
- 支援各種音頻視訊檔案格式,比如mp3、wav、mp4、asf、rm、rmvb、mkv等。
- 支援本地攝像頭裝置,可指定分辨率、幀率。
- 支援各種視訊流格式,比如rtp、rtsp、rtmp、http等。
- 本地音視訊檔案和網絡音視訊檔案,自動識别檔案長度、播放進度、音量大小、靜音狀态等。
- 檔案可以指定播放位置、調節音量大小、設定靜音狀态等。
- 支援倍速播放檔案,可選0.5倍、1.0倍、2.5倍、5.0倍等速度,相當于慢放和快放。
- 支援開始播放、停止播放、暫停播放、繼續播放。
- 支援抓拍截圖,可指定檔案路徑,可選抓拍完成是否自動顯示預覽。
- 支援錄像存儲,手動開始錄像、停止錄像,部分核心支援暫停錄像後繼續錄像,跳過不需要錄像的部分。
- 支援無感覺切換循環播放、自動重連等機制。
- 提供播放成功、播放完成、收到解碼圖檔、收到抓拍圖檔、視訊尺寸變化、錄像狀态變化等信号。
- 多線程處理,一個解碼一個線程,不卡主界面。
5.2 特色功能
- 同時支援多種解碼核心,包括qmedia核心(Qt4/Qt5/Qt6)、ffmpeg核心(ffmpeg2/ffmpeg3/ffmpeg4/ffmpeg5)、vlc核心(vlc2/vlc3)、mpv核心(mpv1/mp2)、海康sdk、easyplayer核心等。
- 非常完善的多重基類設計,新增一種解碼核心隻需要實作極少的代碼量,就可以應用整套機制。
- 同時支援多種畫面顯示政策,自動調整(原始分辨率小于顯示控件尺寸則按照原始分辨率大小顯示,否則等比例縮放)、等比例縮放(永遠等比例縮放)、拉伸填充(永遠拉伸填充)。所有核心和所有視訊顯示模式下都支援三種畫面顯示政策。
- 同時支援多種視訊顯示模式,句柄模式(傳入控件句柄交給對方繪制控制)、繪制模式(回調拿到資料後轉成QImage用QPainter繪制)、GPU模式(回調拿到資料後轉成yuv用QOpenglWidget繪制)。
- 支援多種硬體加速類型,ffmpeg可選dxva2、d3d11va等,mpv可選auto、dxva2、d3d11va,vlc可選any、dxva2、d3d11va。不同的系統環境有不同的類型選擇,比如linux系統有vaapi、vdpau,macos系統有videotoolbox。
- 解碼線程和顯示窗體分離,可指定任意解碼核心挂載到任意顯示窗體,動态切換。
- 支援共享解碼線程,預設開啟并且自動處理,當識别到相同的視訊位址,共享一個解碼線程,在網絡視訊環境中可以大大節約網絡流量以及對方裝置的推流壓力。國内頂尖視訊廠商均采用此政策。這樣隻要拉一路視訊流就可以共享到幾十個幾百個通道展示。
- 自動識别視訊旋轉角度并繪制,比如手機上拍攝的視訊一般是旋轉了90度的,播放的時候要自動旋轉處理,不然預設是倒着的。
- 自動識别視訊流播放過程中分辨率的變化,在視訊控件上自動調整尺寸。比如錄影機可以在使用過程中動态配置分辨率,當分辨率改動後對應視訊控件也要做出同步反應。
- 音視訊檔案無感覺自動切換循環播放,不會出現切換期間黑屏等肉眼可見的切換痕迹。
- 視訊控件同時支援任意解碼核心、任意畫面顯示政策、任意視訊顯示模式。
- 視訊控件懸浮條同時支援句柄、繪制、GPU三種模式,非絕對坐标移來移去。
- 本地攝像頭裝置支援指定裝置名稱、分辨率、幀率進行播放。
- 錄像檔案同時支援打開的視訊檔案、本地攝像頭、網絡視訊流等。
- 瞬間響應打開和關閉,無論是打開不存在的視訊或者網絡流,探測裝置是否存在,讀取中的逾時等待,收到關閉指令立即中斷之前的操作并響應。
- 支援打開各種圖檔檔案,支援本地音視訊檔案拖曳播放。
- 視訊控件懸浮條自帶開始和停止錄像切換、聲音靜音切換、抓拍截圖、關閉視訊等功能。
- 音頻元件支援聲音波形值資料解析,可以根據該值繪制波形曲線和柱狀聲音條,預設提供了聲音振幅信号。
- 各元件中極其詳細的列印資訊提示,尤其是報錯資訊提示,封裝的統一列印格式。針對現場複雜的裝置環境測試極其友善有用,相當于精确定位到具體哪個通道哪個步驟出錯。
- 代碼架構和結構優化到最優,性能強悍,持續疊代更新更新。
- 源碼支援Qt4、Qt5、Qt6,相容所有版本。
5.3 視訊控件
- 可動态添加任意多個osd标簽資訊,标簽資訊包括名字、是否可見、字号大小、文本文字、文本顔色、标簽圖檔、标簽坐标、标簽格式(文本、日期、時間、日期時間、圖檔)、标簽位置(左上角、左下角、右上角、右下角、居中、自定義坐标)。
- 可動态添加任意多個圖形資訊,這個非常有用,比如人工智能算法解析後的圖形區域資訊直接發給視訊控件即可。圖形資訊支援任意形狀,直接繪制在原始圖檔上,采用絕對坐标。
- 圖形資訊包括名字、邊框大小、邊框顔色、背景顔色、矩形區域、路徑集合、點坐标集合等。
- 每個圖形資訊都可指定三種區域中的一種或者多種,指定了的都會繪制。
- 内置懸浮條控件,懸浮條位置支援頂部、底部、左側、右側。
- 懸浮條控件參數包括邊距、間距、背景透明度、背景顔色、文本顔色、按下顔色、位置、按鈕圖示代碼集合、按鈕名稱辨別集合、按鈕提示資訊集合。
- 懸浮條控件一排工具按鈕可自定義,通過結構體參數設定,圖示可選圖形字型還是自定義圖檔。
- 懸浮條按鈕内部實作了錄像切換、抓拍截圖、靜音切換、關閉視訊等功能,也可以自行在源碼中增加自己對應的功能。
- 懸浮條按鈕對應實作了功能的按鈕,有對應圖示切換處理,比如錄像按鈕按下後會切換到正在錄像中的圖示,聲音按鈕切換後變成靜音圖示,再次切換還原。
- 懸浮條按鈕單擊後都用名稱唯一辨別作為信号發出,可以自行關聯響應處理。
- 懸浮條空白區域可以顯示提示資訊,預設顯示目前視訊分辨率大小,可以增加幀率、碼流大小等資訊。
- 視訊控件參數包括邊框大小、邊框顔色、焦點顔色、背景顔色(預設透明)、文字顔色(預設全局文字顔色)、填充顔色(視訊外的空白處填充黑色)、背景文字、背景圖檔(如果設定了圖檔優先取圖檔)、是否拷貝圖檔、縮放顯示模式(自動調整、等比例縮放、拉伸填充)、視訊顯示模式(句柄、繪制、GPU)、啟用懸浮條、懸浮條尺寸(橫向為高度、縱向為寬度)、懸浮條位置(頂部、底部、左側、右側)。
原文連結:Qt音視訊開發38-ffmpeg視訊暫停錄制的設計 - 掘金