一、前言
動态切換解碼核心這個需求也是源自客戶的真實需求,既然是動态切換,那肯定是運作期間切換,而不是通過改變标志位重新編譯程式來切換,最開始做的就是這種方式,這樣就是實作起來簡單,但是用起來不夠友善,随着程式設計架構技術的精進,抽象基類的運用水準逐漸提升,發現視訊控件UI層可以不用變,不同核心發出的信号一樣,UI層接收信号處理就好,至于底層的解碼線程,可以動态銷毀和指定,可以任意指定使用某種解碼線程(ffmpeg核心、vlc核心、mpv核心、廠家sdk核心等),指定後綁定信号到UI就好,UI可以不用管是哪一種核心,拿到資料繪制就好。
為何要這樣設計?一方面可以很友善的做不同核心之間差異的對比; 一方面為何滿足不同使用者需求,比如某些特定格式的檔案用vlc可以正常播放,那就可以選擇切換到這個核心就好,海康的視訊流帶了人工智能分析後的方框等,那就用海康的sdk核心就好,當然最通用跨平台最好的當然是ffmpeg核心,性能也是最好的。
- VideoCore_QMedia=采用qmedia解析(qt自帶且依賴本地解碼器且部分平台支援)
- VideoCore_FFmpeg=采用ffmpeg解析(通用性最好)
- VideoCore_Vlc=采用vlc解析(支援本地檔案最好)
- VideoCore_Mpv=采用mpv解析(支援本地檔案最好且跨平台最多)
- VideoCore_Qtav=采用qtav解析(架構結構最好/基于ffmpeg)
- VideoCore_HaiKang=采用海康sdk解析
- VideoCore_DaHua=采用大華sdk解析
- VideoCore_YuShi=采用宇視sdk解析
- VideoCore_EasyPlayer=采用easyplayer解析
二、效果圖
三、體驗位址
- 國内站點:https://gitee.com/feiyangqingyun
- 國際站點:https://github.com/feiyangqingyun
- 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 體驗位址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 檔案名:bin_video_demo/bin_linux_video。
四、相關代碼
void VideoWidget::connectThreadSignal()
{
if (!videoThread) {
return;
}
//後面帶個參數指定信号唯一(如果多次連接配接信号會自動去重)
connect(videoThread, SIGNAL(started()), this, SLOT(started()), Qt::UniqueConnection);
connect(videoThread, SIGNAL(finished()), this, SLOT(finished()), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receivePlayStart(int)), this, SLOT(receivePlayStart(int)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receivePlayFinsh()), this, SLOT(receivePlayFinsh()), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveImage(QImage, int)), this, SLOT(receiveImage(QImage, int)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(snapImage(QImage, QString)), this, SLOT(snapImage(QImage, QString)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, int)), this, SLOT(receiveFrame(int, int, quint8 *, int)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, quint8 *, quint8 *, quint32, quint32, quint32)),
this, SLOT(receiveFrame(int, int, quint8 *, quint8 *, quint8 *, quint32, quint32, quint32)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, quint8 *, quint32, quint32)),
this, SLOT(receiveFrame(int, int, quint8 *, quint8 *, quint32, quint32)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveLevel(qreal, qreal)), this, SIGNAL(sig_receiveLevel(qreal, qreal)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receivePlayStart(int)), this, SIGNAL(sig_receivePlayStart(int)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receivePlayFinsh()), this, SIGNAL(sig_receivePlayFinsh()), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receivePlayFinsh()), bannerWidget, SLOT(receivePlayFinsh()), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveMuted(bool)), bannerWidget, SLOT(receiveMuted(bool)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(recorderStateChanged(RecorderState, QString)), bannerWidget, SLOT(recorderStateChanged(RecorderState, QString)), Qt::UniqueConnection);
connect(videoThread, SIGNAL(receiveSizeChanged()), this, SLOT(receiveSizeChanged()));
//根據預設音量大小和靜音狀态觸發下信号
if (videoPara.videoCore == VideoCore_FFmpeg) {
QMetaObject::invokeMethod(videoThread, "receiveVolume", Q_ARG(int, widgetPara.soundValue));
QMetaObject::invokeMethod(videoThread, "receiveMuted", Q_ARG(bool, widgetPara.soundMuted));
}
}
void VideoWidget::disconnectThreadSignal()
{
if (!videoThread) {
return;
}
disconnect(videoThread, SIGNAL(started()), this, SLOT(started()));
disconnect(videoThread, SIGNAL(finished()), this, SLOT(finished()));
disconnect(videoThread, SIGNAL(receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
disconnect(videoThread, SIGNAL(receivePlayFinsh()), this, SLOT(receivePlayFinsh()));
disconnect(videoThread, SIGNAL(receiveImage(QImage, int)), this, SLOT(receiveImage(QImage, int)));
disconnect(videoThread, SIGNAL(snapImage(QImage, QString)), this, SLOT(snapImage(QImage, QString)));
disconnect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, int)), this, SLOT(receiveFrame(int, int, quint8 *, int)));
disconnect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, quint8 *, quint8 *, quint32, quint32, quint32)),
this, SLOT(receiveFrame(int, int, quint8 *, quint8 *, quint8 *, quint32, quint32, quint32)));
disconnect(videoThread, SIGNAL(receiveFrame(int, int, quint8 *, quint8 *, quint32, quint32)),
this, SLOT(receiveFrame(int, int, quint8 *, quint8 *, quint32, quint32)));
disconnect(videoThread, SIGNAL(receiveLevel(qreal, qreal)), this, SIGNAL(sig_receiveLevel(qreal, qreal)));
disconnect(videoThread, SIGNAL(receivePlayStart(int)), this, SIGNAL(sig_receivePlayStart(int)));
disconnect(videoThread, SIGNAL(receivePlayFinsh()), this, SIGNAL(sig_receivePlayFinsh()));
disconnect(videoThread, SIGNAL(receivePlayFinsh()), bannerWidget, SLOT(receivePlayFinsh()));
disconnect(videoThread, SIGNAL(receiveMuted(bool)), bannerWidget, SLOT(receiveMuted(bool)));
disconnect(videoThread, SIGNAL(recorderStateChanged(RecorderState, QString)), bannerWidget, SLOT(recorderStateChanged(RecorderState, QString)));
disconnect(videoThread, SIGNAL(receiveSizeChanged()), this, SLOT(receiveSizeChanged()));
}
bool VideoWidget::init()
{
//視訊位址不能為空
if (videoPara.videoUrl.isEmpty()) {
return false;
}
//如果沒有解碼核心則不用繼續
if (videoPara.videoCore == VideoCore_None) {
return false;
}
//初始化參數
VideoHelper::initPara(widgetPara, videoPara);
//線程正在運作不用繼續
if (isRunning) {
return false;
}
//句柄模式則句柄控件在前否則遮罩控件在前
if (widgetPara.videoMode == VideoMode_Hwnd) {
coverWidget->stackUnder(hwndWidget);
} else {
hwndWidget->stackUnder(coverWidget);
}
//已經存在同名的線程則取同名線程
VideoThread *thread = VideoThread::getVideoThread(widgetPara, videoPara);
if (thread) {
isShared = true;
videoThread = thread;
//預設音量大小和靜音狀态取共享線程的
widgetPara.soundValue = thread->getVolume();
widgetPara.soundMuted = thread->getMuted();
//硬體加速也要取共享線程的
hardware = thread->getHardware();
videoPara.hardware = thread->getHardware();
} else {
//建立新的采集線程
videoThread = VideoHelper::newVideoThread(hwndWidget, videoPara.videoCore);
//設定視訊通道唯一辨別
videoThread->setFlag(widgetPara.videoFlag);
//加入到采集線程隊列
if (widgetPara.sharedData) {
VideoThread::videoThreads << videoThread;
}
//設定對應參數
videoThread->setVideoMode(widgetPara.videoMode);
VideoHelper::initVideoThread(videoThread, videoPara);
}
//綁定信号槽
connectThreadSignal();
return true;
}
bool VideoWidget::open(const QString &videoUrl)
{
//線程正常說明還在運作需要先停止
if (videoThread) {
this->stop();
qApp->processEvents();
}
//重新初始化和播放
videoPara.videoUrl = videoUrl;
if (this->init()) {
this->play();
return true;
} else {
return false;
}
}
void VideoWidget::play()
{
//如果是圖檔則隻顯示圖檔就行
image = QImage(videoPara.videoUrl);
if (!image.isNull()) {
videoThread->setVideoSize(QString("%1x%2").arg(image.width()).arg(image.height()));
this->setImage(image);
return;
}
//采用已經存在的采集線程如果處于解碼正常階段則還要發幾個信号通知
if (videoThread->getIsOk()) {
QMetaObject::invokeMethod(this, "receivePlayStart", Qt::DirectConnection, Q_ARG(int, 0));
QMetaObject::invokeMethod(videoThread, "receiveSizeChanged", Qt::DirectConnection);
QMetaObject::invokeMethod(videoThread, "receiveDuration", Qt::DirectConnection, Q_ARG(qint64, videoThread->getDuration()));
}
//已經在運作階段還要發送已經開始的信号
if (videoThread->isRunning()) {
isRunning = true;
QMetaObject::invokeMethod(this, "sig_receivePlayStart", Qt::DirectConnection, Q_ARG(int, 0));
}
//啟動播放線程
videoThread->play();
//初始化标簽資訊和圖形資訊
this->osdChanged();
this->graphChanged();
}
void VideoWidget::stop()
{
//立即隐藏懸浮條
bannerWidget->setVisible(false);
//關閉的時候将遮罩控件移到最前
hwndWidget->stackUnder(coverWidget);
//處于運作狀态才可以停止
if (videoThread && videoThread->isRunning()) {
//先判斷目前線程的引用計數是0才需要真正停止
if (videoThread->refCount > 0) {
//減少引用計數
videoThread->refCount--;
//執行停止信号
QMetaObject::invokeMethod(this, "receivePlayFinsh", Qt::DirectConnection);
QMetaObject::invokeMethod(this, "sig_receivePlayFinsh", Qt::DirectConnection);
//取消信号關聯
disconnectThreadSignal();
videoThread = NULL;
} else {
//停止播放
videoThread->stop();
//取消信号關聯
disconnectThreadSignal();
//從隊列中移除
VideoThread::videoThreads.removeOne(videoThread);
//釋放線程
videoThread->debug("删除線程", "");
videoThread->deleteLater();
videoThread = NULL;
}
}
//複位标志位并将線程置空(沒有這個的話可能會出現野指針的情況)
isRunning = false;
isShared = false;
videoThread = NULL;
videoPara.reset();
AbstractVideoWidget::clear();
}
五、功能特點
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)、啟用懸浮條、懸浮條尺寸(橫向為高度、縱向為寬度)、懸浮條位置(頂部、底部、左側、右側)。
5.4 核心ffmpeg
- 支援各種音視訊檔案、本地攝像頭裝置,各種視訊流網絡流。
- 支援開始播放、暫停播放、繼續播放、停止播放、設定播放進度、倍速播放。
- 可設定音量、靜音切換、抓拍圖檔、錄像存儲。
- 自動提取專輯資訊比如标題、藝術家、專輯、專輯封面,自動顯示專輯封面。
- 完美支援音視訊同步和倍速播放。
- 解碼政策支援速度優先、品質優先、均衡處理、最快速度。
- 支援手機視訊旋轉角度顯示,比如一般手機拍攝的視訊是旋轉了90度的,解碼顯示的時候需要重新旋轉90度才是正的。
- 自動轉換yuv420格式,比如本地攝像頭是yuyv422格式,有些視訊檔案是xx格式,統一将非yuv420格式轉換,然後再進行處理。
- 支援硬解碼dxva2、d3d11va等,性能極高尤其是大分辨率比如4K視訊。
- 視訊響應極低延遲0.2s左右,極速響應打開視訊流0.5s左右,專門做了優化處理。
- 硬解碼和GPU繪制組合,極低CPU占用,比海康大華等用戶端更優。
- 支援視訊流中的各種音頻格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支援,推薦選擇AAC相容性跨平台性最好。
- 視訊存儲支援yuv、h264、mp4多種格式,音頻存儲支援pcm、wav、aac多種格式。預設視訊mp4格式、音頻aac格式。
- 支援分開存儲音頻視訊檔案,也支援合并到一個mp4檔案,預設政策是無論何種音視訊檔案格式存儲,最終都轉成mp4及aac格式,然後合并成音視訊一起的mp4檔案。
- 支援本地攝像頭實時視訊顯示帶音頻輸入輸出,音視訊錄制合并到一個mp4檔案。
- 支援H264/H265編碼(現在越來越多的監控攝像頭是H265視訊流格式)生成視訊檔案,内部自動識别切換編碼格式。
- 自動識别視訊流動态分辨率改動,重新打開視訊流。
- 支援使用者資訊中包含特殊字元(比如使用者資訊中包含+#@等字元)的視訊流播放,内置解析轉義處理。
- 純qt+ffmpeg解碼,非sdl等第三方繪制播放依賴,gpu繪制采用qopenglwidget,音頻播放采用qaudiooutput。
- 同時支援ffmpeg2、ffmpeg3、ffmpeg4、ffmpeg5版本,全部做了相容處理。如果需要支援xp需要選用ffmpeg3及以下。