天天看點

Qt音視訊開發13-視訊解碼線程基類的設計

一、前言

這個解碼線程基類的設計,是到目前為止個人覺得自己設計的最好的基類之一,當然也不是一開始就知道這樣設計,沒有個三五年的摸爬滾打以及社會的毒打,是想不到要這樣設計的,一方面是要不斷提煉各種視訊類視訊元件的共性作為局部變量,比如通用的參數有視訊畫面寬度videoWidth、視訊畫面高度videoHeight、視訊畫面幀率frameRate、視訊旋轉角度rotate、音頻采樣率sampleRate、音頻通道數channelCount等。這些共性參數都是在瘋狂實戰的過程中提煉的,久而久之就出來了。

翻閱Qt源碼本身,他裡面也基本上都是這種設計思路,有個巨大的好處可以複用這些代碼,子類隻要繼承了這個類,就能直接使用,不需要還在子類中寫這些參數變量,函數接口隻需要重載實作即可,重載實作了的就調用子類的實作,沒有重載的就調用基類的實作,這也是為何c++如此流行的一個重要原因,封裝、繼承、多态這三大特性成就了c++。

基類的設計優點優勢巨大,也不是沒有缺點的,缺點就是有部分共性是大部分子類都有的,但是部分沒有,或者部分子類沒有去實作重載,是以使用的時候萬一執行個體化的子類沒有去實作對應接口,就算你調用了,可能沒效果,因為預設是基類的,是以有的時候就會很納悶為何明明調用了函數,但是沒有效果,仔細跟進一查原來子類并沒有去實作接口,可能該子類是本來就不需要這個接口。

二、效果圖

Qt音視訊開發13-視訊解碼線程基類的設計

三、體驗位址

  1. 國内站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 體驗位址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 檔案名:bin_video_demo/bin_linux_video。

四、相關代碼

void AbstractVideoThread::debug(const QString &head, const QString &msg, const QString &url)
{
    if (debugInfo == 0) {
        return;
    }

    //如果設定了唯一辨別則放在列印前面
    QString text = head;
    if (!flag.isEmpty()) {
        text = QString("辨別[%1] %2").arg(flag).arg(text);
    }

    QString addr = url;
    if (debugInfo == 2) {
        //為空的時候擷取一次即可
        if (ip.isEmpty()) {
            ip = UrlHelper::getUrlIP(url);
        }
        addr = ip;
    }

    if (msg.isEmpty()) {
        qDebug() << TIMEMS << QString("%1 -> 位址: %2").arg(text).arg(addr);
    } else {
        qDebug() << TIMEMS << QString("%1 -> %2 位址: %3").arg(text).arg(msg).arg(addr);
    }
}

void AbstractVideoThread::setImage(const QImage &image)
{
    lastTime = QDateTime::currentDateTime();
    emit receiveImage(image, 0);
}

void AbstractVideoThread::setRgb(int width, int height, quint8 *dataRGB, int type)
{
    lastTime = QDateTime::currentDateTime();
    emit receiveFrame(width, height, dataRGB, type);
}

void AbstractVideoThread::setYuv(int width, int height, quint8 *dataY, quint8 *dataU, quint8 *dataV)
{
    lastTime = QDateTime::currentDateTime();
    emit receiveFrame(width, height, dataY, dataU, dataV, width, width / 2, width / 2);
}

void AbstractVideoThread::play()
{
    //沒有運作才需要啟動
    if (!this->isRunning()) {
        stopped = false;
        isPause = false;
        isSnap = false;
        this->start();
    }
}

void AbstractVideoThread::stop()
{
    //處于運作狀态才可以停止
    if (this->isRunning()) {
        stopped = true;
        isPause = false;
        isSnap = false;
        this->wait();
    }
}

void AbstractVideoThread::pause()
{
    if (this->isRunning()) {
        isPause = true;
    }
}

void AbstractVideoThread::next()
{
    if (this->isRunning()) {
        isPause = false;
    }
}

void AbstractVideoThread::snap(const QString &snapName)
{
    if (this->isRunning()) {
        isSnap = true;
        this->setSnapName(snapName);
    }
}

void AbstractVideoThread::snapFinsh(const QImage &image)
{
    //如果填了截圖檔案名稱則先儲存圖檔
    if (!snapName.isEmpty() && !image.isNull()) {
        //取出拓展名根據拓展名儲存格式
        QString suffix = snapName.split(".").last();
        image.save(snapName, suffix.toLatin1().constData());
    }

    //發送截圖完成信号
    emit snapImage(image, snapName);
}

void AbstractVideoThread::recordStart(const QString &fileName)
{
#ifdef videosave
    this->setFileName(fileName);
    if (saveAudioType > 0) {
        //處于暫停階段則切換暫停标志位(暫停後再次恢複說明又重新開始錄制)
        if (saveAudio->getIsPause()) {
            saveAudio->pause();
            emit recorderStateChanged(RecorderState_Recording, saveAudio->getFileName());
        } else {
            saveAudio->setPara(saveAudioType, sampleRate, channelCount, profile);
            saveAudio->open(fileName);
            if (saveAudio->getIsOk()) {
                emit recorderStateChanged(RecorderState_Recording, saveAudio->getFileName());
            }
        }
    }

    if (saveVideoType == SaveVideoType_Yuv && !onlyAudio) {
        //處于暫停階段則切換暫停标志位(暫停後再次恢複說明又重新開始錄制)
        if (saveVideo->getIsPause()) {
            isRecord = true;
            saveVideo->pause();
            emit recorderStateChanged(RecorderState_Recording, saveVideo->getFileName());
        } else {
            saveVideo->setPara(saveVideoType, videoWidth, videoHeight, frameRate);
            saveVideo->open(fileName);
            if (saveVideo->getIsOk()) {
                isRecord = true;
                emit recorderStateChanged(RecorderState_Recording, saveVideo->getFileName());
            }
        }
    }
#endif
}

void AbstractVideoThread::recordPause()
{
#ifdef videosave
    if (saveAudioType > 0) {
        if (saveAudio->getIsOk()) {
            saveAudio->pause();
            emit recorderStateChanged(RecorderState_Paused, saveAudio->getFileName());
        }
    }

    if (saveVideoType == SaveVideoType_Yuv && !onlyAudio) {
        if (saveVideo->getIsOk()) {
            isRecord = false;
            saveVideo->pause();
            emit recorderStateChanged(RecorderState_Paused, saveVideo->getFileName());
        }
    }
#endif
}

void AbstractVideoThread::recordStop()
{
#ifdef videosave
    if (saveAudioType > 0) {
        if (saveAudio->getIsOk()) {
            saveAudio->stop();
            emit recorderStateChanged(RecorderState_Stopped, saveAudio->getFileName());
        }
    }

    if (saveVideoType == SaveVideoType_Yuv && !onlyAudio) {
        if (saveVideo->getIsOk()) {
            isRecord = false;
            saveVideo->stop();
            emit recorderStateChanged(RecorderState_Stopped, saveVideo->getFileName());
        }
    }
#endif
}

void AbstractVideoThread::writeVideoData(int width, int height, quint8 *dataY, quint8 *dataU, quint8 *dataV)
{
#ifdef videosave
    if (saveVideoType == SaveVideoType_Yuv && isRecord) {
        saveVideo->setPara(SaveVideoType_Yuv, width, height, frameRate);
        saveVideo->write(dataY, dataU, dataV);
    }
#endif
}

void AbstractVideoThread::writeAudioData(const char *data, qint64 len)
{
#ifdef videosave
    if (saveAudioType > 0 && saveAudio->getIsOk()) {
        saveAudio->write(data, len);
    }
#endif
}

void AbstractVideoThread::writeAudioData(const QByteArray &data)
{
#ifdef videosave
    if (saveAudioType > 0 && saveAudio->getIsOk()) {
        this->writeAudioData(data.constData(), data.length());
    }
#endif
}

void AbstractVideoThread::setOsdInfo(const QList<OsdInfo> &listOsd)
{
    this->listOsd = listOsd;
}

void AbstractVideoThread::setGraphInfo(const QList<GraphInfo> &listGraph)
{
    this->listGraph = listGraph;
}           

五、功能特點

5.1 基礎功能

  1. 支援各種音頻視訊檔案格式,比如mp3、wav、mp4、asf、rm、rmvb、mkv等。
  2. 支援本地攝像頭裝置,可指定分辨率、幀率。
  3. 支援各種視訊流格式,比如rtp、rtsp、rtmp、http等。
  4. 本地音視訊檔案和網絡音視訊檔案,自動識别檔案長度、播放進度、音量大小、靜音狀态等。
  5. 檔案可以指定播放位置、調節音量大小、設定靜音狀态等。
  6. 支援倍速播放檔案,可選0.5倍、1.0倍、2.5倍、5.0倍等速度,相當于慢放和快放。
  7. 支援開始播放、停止播放、暫停播放、繼續播放。
  8. 支援抓拍截圖,可指定檔案路徑,可選抓拍完成是否自動顯示預覽。
  9. 支援錄像存儲,手動開始錄像、停止錄像,部分核心支援暫停錄像後繼續錄像,跳過不需要錄像的部分。
  10. 支援無感覺切換循環播放、自動重連等機制。
  11. 提供播放成功、播放完成、收到解碼圖檔、收到抓拍圖檔、視訊尺寸變化、錄像狀态變化等信号。
  12. 多線程處理,一個解碼一個線程,不卡主界面。

5.2 特色功能

  1. 同時支援多種解碼核心,包括qmedia核心(Qt4/Qt5/Qt6)、ffmpeg核心(ffmpeg2/ffmpeg3/ffmpeg4/ffmpeg5)、vlc核心(vlc2/vlc3)、mpv核心(mpv1/mp2)、海康sdk、easyplayer核心等。
  2. 非常完善的多重基類設計,新增一種解碼核心隻需要實作極少的代碼量,就可以應用整套機制。
  3. 同時支援多種畫面顯示政策,自動調整(原始分辨率小于顯示控件尺寸則按照原始分辨率大小顯示,否則等比例縮放)、等比例縮放(永遠等比例縮放)、拉伸填充(永遠拉伸填充)。所有核心和所有視訊顯示模式下都支援三種畫面顯示政策。
  4. 同時支援多種視訊顯示模式,句柄模式(傳入控件句柄交給對方繪制控制)、繪制模式(回調拿到資料後轉成QImage用QPainter繪制)、GPU模式(回調拿到資料後轉成yuv用QOpenglWidget繪制)。
  5. 支援多種硬體加速類型,ffmpeg可選dxva2、d3d11va等,mpv可選auto、dxva2、d3d11va,vlc可選any、dxva2、d3d11va。不同的系統環境有不同的類型選擇,比如linux系統有vaapi、vdpau,macos系統有videotoolbox。
  6. 解碼線程和顯示窗體分離,可指定任意解碼核心挂載到任意顯示窗體,動态切換。
  7. 支援共享解碼線程,預設開啟并且自動處理,當識别到相同的視訊位址,共享一個解碼線程,在網絡視訊環境中可以大大節約網絡流量以及對方裝置的推流壓力。國内頂尖視訊廠商均采用此政策。這樣隻要拉一路視訊流就可以共享到幾十個幾百個通道展示。
  8. 自動識别視訊旋轉角度并繪制,比如手機上拍攝的視訊一般是旋轉了90度的,播放的時候要自動旋轉處理,不然預設是倒着的。
  9. 自動識别視訊流播放過程中分辨率的變化,在視訊控件上自動調整尺寸。比如錄影機可以在使用過程中動态配置分辨率,當分辨率改動後對應視訊控件也要做出同步反應。
  10. 音視訊檔案無感覺自動切換循環播放,不會出現切換期間黑屏等肉眼可見的切換痕迹。
  11. 視訊控件同時支援任意解碼核心、任意畫面顯示政策、任意視訊顯示模式。
  12. 視訊控件懸浮條同時支援句柄、繪制、GPU三種模式,非絕對坐标移來移去。
  13. 本地攝像頭裝置支援指定裝置名稱、分辨率、幀率進行播放。
  14. 錄像檔案同時支援打開的視訊檔案、本地攝像頭、網絡視訊流等。
  15. 瞬間響應打開和關閉,無論是打開不存在的視訊或者網絡流,探測裝置是否存在,讀取中的逾時等待,收到關閉指令立即中斷之前的操作并響應。
  16. 支援打開各種圖檔檔案,支援本地音視訊檔案拖曳播放。
  17. 視訊控件懸浮條自帶開始和停止錄像切換、聲音靜音切換、抓拍截圖、關閉視訊等功能。
  18. 音頻元件支援聲音波形值資料解析,可以根據該值繪制波形曲線和柱狀聲音條,預設提供了聲音振幅信号。
  19. 各元件中極其詳細的列印資訊提示,尤其是報錯資訊提示,封裝的統一列印格式。針對現場複雜的裝置環境測試極其友善有用,相當于精确定位到具體哪個通道哪個步驟出錯。
  20. 代碼架構和結構優化到最優,性能強悍,持續疊代更新更新。
  21. 源碼支援Qt4、Qt5、Qt6,相容所有版本。

5.3 視訊控件

  1. 可動态添加任意多個osd标簽資訊,标簽資訊包括名字、是否可見、字号大小、文本文字、文本顔色、标簽圖檔、标簽坐标、标簽格式(文本、日期、時間、日期時間、圖檔)、标簽位置(左上角、左下角、右上角、右下角、居中、自定義坐标)。
  2. 可動态添加任意多個圖形資訊,這個非常有用,比如人工智能算法解析後的圖形區域資訊直接發給視訊控件即可。圖形資訊支援任意形狀,直接繪制在原始圖檔上,采用絕對坐标。
  3. 圖形資訊包括名字、邊框大小、邊框顔色、背景顔色、矩形區域、路徑集合、點坐标集合等。
  4. 每個圖形資訊都可指定三種區域中的一種或者多種,指定了的都會繪制。
  5. 内置懸浮條控件,懸浮條位置支援頂部、底部、左側、右側。
  6. 懸浮條控件參數包括邊距、間距、背景透明度、背景顔色、文本顔色、按下顔色、位置、按鈕圖示代碼集合、按鈕名稱辨別集合、按鈕提示資訊集合。
  7. 懸浮條控件一排工具按鈕可自定義,通過結構體參數設定,圖示可選圖形字型還是自定義圖檔。
  8. 懸浮條按鈕内部實作了錄像切換、抓拍截圖、靜音切換、關閉視訊等功能,也可以自行在源碼中增加自己對應的功能。
  9. 懸浮條按鈕對應實作了功能的按鈕,有對應圖示切換處理,比如錄像按鈕按下後會切換到正在錄像中的圖示,聲音按鈕切換後變成靜音圖示,再次切換還原。
  10. 懸浮條按鈕單擊後都用名稱唯一辨別作為信号發出,可以自行關聯響應處理。
  11. 懸浮條空白區域可以顯示提示資訊,預設顯示目前視訊分辨率大小,可以增加幀率、碼流大小等資訊。
  12. 視訊控件參數包括邊框大小、邊框顔色、焦點顔色、背景顔色(預設透明)、文字顔色(預設全局文字顔色)、填充顔色(視訊外的空白處填充黑色)、背景文字、背景圖檔(如果設定了圖檔優先取圖檔)、是否拷貝圖檔、縮放顯示模式(自動調整、等比例縮放、拉伸填充)、視訊顯示模式(句柄、繪制、GPU)、啟用懸浮條、懸浮條尺寸(橫向為高度、縱向為寬度)、懸浮條位置(頂部、底部、左側、右側)。

5.4 核心ffmpeg

  1. 支援各種音視訊檔案、本地攝像頭裝置,各種視訊流網絡流。
  2. 支援開始播放、暫停播放、繼續播放、停止播放、設定播放進度、倍速播放。
  3. 可設定音量、靜音切換、抓拍圖檔、錄像存儲。
  4. 自動提取專輯資訊比如标題、藝術家、專輯、專輯封面,自動顯示專輯封面。
  5. 完美支援音視訊同步和倍速播放。
  6. 解碼政策支援速度優先、品質優先、均衡處理、最快速度。
  7. 支援手機視訊旋轉角度顯示,比如一般手機拍攝的視訊是旋轉了90度的,解碼顯示的時候需要重新旋轉90度才是正的。
  8. 自動轉換yuv420格式,比如本地攝像頭是yuyv422格式,有些視訊檔案是xx格式,統一将非yuv420格式轉換,然後再進行處理。
  9. 支援硬解碼dxva2、d3d11va等,性能極高尤其是大分辨率比如4K視訊。
  10. 視訊響應極低延遲0.2s左右,極速響應打開視訊流0.5s左右,專門做了優化處理。
  11. 硬解碼和GPU繪制組合,極低CPU占用,比海康大華等用戶端更優。
  12. 支援視訊流中的各種音頻格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支援,推薦選擇AAC相容性跨平台性最好。
  13. 視訊存儲支援yuv、h264、mp4多種格式,音頻存儲支援pcm、wav、aac多種格式。預設視訊mp4格式、音頻aac格式。
  14. 支援分開存儲音頻視訊檔案,也支援合并到一個mp4檔案,預設政策是無論何種音視訊檔案格式存儲,最終都轉成mp4及aac格式,然後合并成音視訊一起的mp4檔案。
  15. 支援本地攝像頭實時視訊顯示帶音頻輸入輸出,音視訊錄制合并到一個mp4檔案。
  16. 支援H264/H265編碼(現在越來越多的監控攝像頭是H265視訊流格式)生成視訊檔案,内部自動識别切換編碼格式。
  17. 自動識别視訊流動态分辨率改動,重新打開視訊流。
  18. 支援使用者資訊中包含特殊字元(比如使用者資訊中包含+#@等字元)的視訊流播放,内置解析轉義處理。
  19. 純qt+ffmpeg解碼,非sdl等第三方繪制播放依賴,gpu繪制采用qopenglwidget,音頻播放采用qaudiooutput。
  20. 同時支援ffmpeg2、ffmpeg3、ffmpeg4、ffmpeg5版本,全部做了相容處理。如果需要支援xp需要選用ffmpeg3及以下。

繼續閱讀