日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计

發(fā)布時間:2023/12/13 编程问答 67 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

與解碼相關(guān)的主要代碼在上一篇博客中已經(jīng)做了介紹,本篇我們會先討論一下如何控制解碼速度再提供一個我個人的封裝思路。最后回歸到界面設(shè)計環(huán)節(jié)重點(diǎn)看一下如何保證播放器界面在縮放和拖動的過程中保證視頻畫面的寬高比例。

一、解碼速度

播放器播放媒體文件的時候播放進(jìn)度需要我們自己控制。基本的控制方法有兩種:

  • 根據(jù)FPS控制視頻的播放幀率,讓音頻跟隨。
  • 控制音頻的播放解碼速度,讓視頻跟隨。
  • 媒體文件在編碼的時候,正常情況下視頻數(shù)據(jù)和音頻輸出是交替寫入的。換句話說,解碼每一幀視頻數(shù)據(jù)伴隨需要播放的音頻數(shù)據(jù)也應(yīng)該被解碼。所以,方案一的實(shí)現(xiàn)就比較簡單和直接。但是在有些情況下也可能會出現(xiàn)音視頻編碼不同步的問題,大部分情況是視頻提前于音頻。萬一遇到這樣的情況,如果需要讓我們的播放器帶有一定糾錯功能就必須采用第二種方案。方案二的設(shè)計思路是當(dāng)遇到音頻數(shù)據(jù)時正常播放,遇到視頻數(shù)據(jù)時先緩沖起來,再根據(jù)pts參數(shù)同步。

    方案一

    QTime t; QIODevice ioDevice; t.restart(); AVPacket *pkt = readPacket(); if (pkt->stream_index == videoIndex) { // 當(dāng)前為視頻幀,計算視頻播放每幀的間隔時間(1000/fps) - 解碼消耗的時間(毫秒) = 實(shí)際解碼間隔時間interval codecPacket(pkt);int el = t.elapsed();int interval = 1000 / fps - el > 0 ? 1000 / fps - el : 1;QThread::msleep(interval); } else if (pkt->stream_index == audioIndex) { // 當(dāng)前為音頻幀,直接讓Qt的音頻播放器播放 codecPacket(pkt);char data[10000] = { 0 };int len = toPCM(data);ioDevice->write(data, len); }

    方案二

    AVPacket *pkt = readPacket();if (pkt->stream_index == audioIndex) {codecPacket(pkt);char data[AUDIO_IODEVICE_WRITE_SIZE] = { 0 };int len = toPCM(data);ioDevice->write(data, len); } else if (pkt->stream_index == videoIndex) {videoPacketList.push_back(pkt); }while (videoPacketList.size() > 0 && videoPts < audioPts) {AVPacket *pkt = videoPacketList.front();videoPacketList.pop_front();codecPacket(pkt); }

    這個方案遇到的另外一個問題是我們?nèi)绾潍@取videoPts和audioPts這兩個值。我個人的解決思路是在解碼環(huán)節(jié)進(jìn)行,即,每次對pkt進(jìn)行一次解碼就根據(jù)pkt的stream_index值分別記錄解碼后的AVFrame的pts。不過音頻的pts和視頻的pts不能直接比較。我們還需要根據(jù)各自的AVRational做一次換算。算法如下:

    AVRational r; frame->pts * (double)r.num / (double)r.den;

    二、封裝思路討論

    代碼封裝實(shí)際是一個見仁見智的工作,可能不同的人對代碼結(jié)構(gòu)的理解不同,實(shí)現(xiàn)的封裝方式也會存在差異。包括我們的解決方案到底針對哪些需求也會按照不同的思路做封裝。在這里插一句題外話,大家認(rèn)為程序開發(fā)到底是一種什么樣的工作性質(zhì)?是僅僅為了實(shí)現(xiàn)客戶的需求嗎?如果你只能理解到這一層,那恐怕還遠(yuǎn)遠(yuǎn)不夠!客戶需求只能算是拋給你的一個問題,而你反饋給客戶的應(yīng)該是一套合理的解決方案。從這個觀點(diǎn)出發(fā)我們進(jìn)行再抽象,程序開發(fā)應(yīng)該是一種從問題空間到解空間的映射。既然如此,我們就不能將自己的工作僅僅停留在功能實(shí)現(xiàn)這個層面,我們還應(yīng)該提供更好的解決思路——最佳實(shí)踐。

    基本上,如果我們只需要設(shè)計一個簡單的播放器。大概需要三個模塊的支持:

    界面模塊(av_player):包括了界面的樣式和基礎(chǔ)互動功能

    解碼模塊(Decoder):這個部分主要通過對FFmpeg的功能二次封裝,并對外提供接口支持

    播放器模塊(PlayerWidget):負(fù)責(zé)界面和解碼模塊的連接,界面中嵌入播放器模塊,視頻顯示和音頻播放都由播放器模塊獨(dú)立負(fù)責(zé)。

    下面看一下我設(shè)計的解碼模塊對外提供的接口:Decoder.h

    class Decoder : protected QThread { public:Decoder();virtual ~Decoder();bool open(const char *filename);void close();// 從文件中讀取一個壓縮報文AVPacket* readPacket();// 解碼報文并釋放空間,返回值為當(dāng)前解碼報文的pts時間(毫秒)int codecPacket(AVPacket* pkt);// 將解碼幀F(xiàn)rame轉(zhuǎn)碼為RGB或PCMint toRGB(char *outData, int outWidth, int outHeight);int toPCM(char *outData);int durationMsec; // 文件時長int fps; // 視頻FPSint srcWidth; // 視頻寬度int srcHeight; // 視頻高度int videoIndex; // 視頻通道int audioIndex; // 音頻通道int sampleRate; // 音頻采樣率int channels; // 聲道int sampleSize; // 樣本位數(shù)bool endFlag; // 線程結(jié)束標(biāo)志bool pauseFlag; // 線程暫停標(biāo)志// 記錄當(dāng)前的音視頻所處在的pts時間戳(毫秒)int videoPts;int audioPts;// 記錄音視頻的編解碼格式int sampleFmt;int pixFmt;/************************************************************************//* default: CD音質(zhì)(16bit 44100Hz stereo) *//************************************************************************/int dstSampleRate = 44100; // 采樣率int dstSampleSize = 16; // 采樣大小int dstChannels = 2; // 通道數(shù)// 線程啟動的代理方法void start();// 音頻輸出QAudioOutput *audioOutput = NULL; protected:void run(); private:QMutex mtx;AVFormatContext *pFormatCtx = NULL;SwsContext *videoSwsCtx = NULL;AVFrame *yuv = NULL;SwrContext *audioSwrCtx = NULL;AVFrame *pcm = NULL;QIODevice *ioDevice = NULL;std::list<AVPacket*> videoPacketList;AVInputTypeEnum avType = AVInputTypeEnum::NOTYPE;QString fileName; };

    乍一看很復(fù)雜,我們稍微理一下思路。首先Decoder繼承了QThread,并重寫了start()方法。重寫的好處是,在對調(diào)用者完全透明的情況下,我們可以在這個函數(shù)中做一些初始化工作。在設(shè)計模式中,它數(shù)據(jù)代理模式。其他方法介紹:

    • bool open(const char *filename):開發(fā)多媒體文件
    • void close():關(guān)閉和析構(gòu)所有編碼,這個步驟在音視頻編解碼的開發(fā)中非常重要
    • AVPacket* readPacket():讀取一幀數(shù)據(jù)并返回
    • int codecPacket(AVPacket* pkt):解碼之前讀取到的一幀數(shù)據(jù),返回該幀數(shù)據(jù)表示的pts值并將傳入的pkt析構(gòu)釋放內(nèi)存空間
    • int toRGB(char *outData, int outWidth, int outHeight):轉(zhuǎn)碼視頻幀,將yuv轉(zhuǎn)換為rgb
    • int toPCM(char *outData):轉(zhuǎn)碼音頻幀

    播放器模塊:PlayerWidget.h

    class PlayerWidget : public QOpenGLWidget { public:PlayerWidget(Decoder *dec, QWidget *parent, int interval);virtual ~PlayerWidget();/************************************************************************//* default: 720p 25fps *//************************************************************************/int videoWidth = 720;int videoHeight = 480;int m_interval = 40;/************************************************************************//* default: CD音質(zhì)(16bit 44100Hz stereo) *//************************************************************************/int sampleRate = 44100; // 采樣率int sampleSize = 16; // 采樣大小int channels = 2; // 通道數(shù) protected:void timerEvent(QTimerEvent *e);void paintEvent(QPaintEvent *e); private:Decoder *decoder = NULL;QAudioOutput *out;QIODevice *io; };

    這個模塊繼承自QOpenGLWidget,并包含了QAudioOutput。這兩個Qt類分別代表了視頻播放和音頻播放。

    界面模塊:在這個模塊中有一個重要的工作就是當(dāng)我們在播放視頻的時候放大和縮小播放器窗口如何保證視頻畫面依然保持正確的寬高比,為此我寫了一個靜態(tài)函數(shù):

    struct AspectRatio {double width;double height; };static AspectRatio* fitRatio(int outWidth, int outHeight, int inWidth, int inHeight) {double r1 = ((double)outWidth / (double)outHeight);double r2 = ((double)inWidth / (double)inHeight);AspectRatio *ar = new AspectRatio;if (r1 > r2) {int newWidth = (double)(outHeight * inWidth) / (double)inHeight;ar->width = newWidth;ar->height = outHeight;return ar;}else {int newHeight = (double)(inHeight * outWidth) / (double)inWidth;ar->width = outWidth;ar->height = newHeight;return ar;} }

    最后附上我自己設(shè)計的播放器界面

    項(xiàng)目源碼:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player

    轉(zhuǎn)載于:https://www.cnblogs.com/learnhow/p/8970893.html

    總結(jié)

    以上是生活随笔為你收集整理的Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。