Qt與FFmpeg聯合開發指南(二)——解碼(2):封裝和界面設計

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

一、解碼速度

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

  1. 根據FPS控制視頻的播放幀率,讓音頻跟隨。
  2. 控制音頻的播放解碼速度,讓視頻跟隨。

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

方案一

QTime t;
QIODevice ioDevice;
t.restart();
AVPacket *pkt = readPacket();
if (pkt->stream_index == videoIndex) { // 當前為視頻幀,計算視頻播放每幀的間隔時間(1000/fps) - 解碼消耗的時間(毫秒) = 實際解碼間隔時間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) { // 當前為音頻幀,直接讓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);
}

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

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

二、封裝思路討論

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

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

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

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

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

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

class Decoder : protected QThread
{
public:Decoder();virtual ~Decoder();bool open(const char *filename);void close();// 從文件中讀取一個壓縮報文AVPacket* readPacket();// 解碼報文并釋放空間,返回值為當前解碼報文的pts時間(毫秒)int codecPacket(AVPacket* pkt);// 將解碼幀Frame轉碼為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; // 樣本位數bool endFlag; // 線程結束標志bool pauseFlag; // 線程暫停標志// 記錄當前的音視頻所處在的pts時間戳(毫秒)int videoPts;int audioPts;// 記錄音視頻的編解碼格式int sampleFmt;int pixFmt;/************************************************************************//* default: CD音質(16bit 44100Hz stereo)                              *//************************************************************************/int dstSampleRate = 44100; // 采樣率int dstSampleSize = 16; // 采樣大小int dstChannels = 2; // 通道數// 線程啟動的代理方法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;
};

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

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

播放器模塊: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音質(16bit 44100Hz stereo)                              *//************************************************************************/int sampleRate = 44100; // 采樣率int sampleSize = 16; // 采樣大小int channels = 2; // 通道數
protected:void timerEvent(QTimerEvent *e);void paintEvent(QPaintEvent *e);
private:Decoder *decoder = NULL;QAudioOutput *out;QIODevice *io;
};

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

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

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;}
}

最后附上我自己設計的播放器界面

項目源碼:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player

轉載于:https://www.cnblogs.com/learnhow/p/8970893.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/250380.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/250380.shtml
英文地址,請注明出處:http://en.pswp.cn/news/250380.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Bzoj1051 受歡迎的牛

每一頭牛的愿望就是變成一頭最受歡迎的牛。現在有 N 頭牛&#xff0c;給你 M 對整數 (A,B)&#xff0c;表示牛 A 認為牛 B 受歡迎。這種關系是具有傳遞性的&#xff0c;如果 A 認為 B 受歡迎&#xff0c;B 認為 C 受歡迎&#xff0c;那么牛 A 也認為牛 C 受歡迎。你的任務是求出…

node --- 模塊加載機制

1. Node.js中模塊加載機制 1.1 模塊查找規則-當模塊擁有路徑但沒有后綴時 require(./find.js); require(./find);require方法根據模塊路徑查找模塊,如果是完整路徑,直接進入模塊如果模塊后綴省略,先找同名JS文件再找同名JS文件夾 require(./find); // 以上會先找到命令行目錄…

51Nod 蜥蜴和地下室(搜索)

哈利喜歡玩角色扮演的電腦游戲《蜥蜴和地下室》。此時&#xff0c;他正在扮演一個魔術師。在最后一關&#xff0c;他必須和一排的弓箭手戰斗。他唯一能消滅他們的辦法是一個火球咒語。如果哈利用他的火球咒語攻擊第i個弓箭手&#xff08;他們從左到右標記&#xff09;&#xff…

多線程——實現Runnable接口實現一個多線程

實現Runnable接口實現一個多線程 Runnable接口源碼&#xff1a; package java.lang; //Runnable接口源碼只有一個run方法 public interface Runnable {public abstract void run(); } 實現Runnable的兩個多線程類&#xff1a; public class RunnableThread1 implements Runnabl…

javascript --- 文件上傳即時預覽 閉包實現多圖片即時預覽

使用javascript原生功能實現,點擊上傳文件,然后再網頁上顯示出來 1. 初級顯示 1.1 準備一個input標簽和一個img標簽 <input typefile id"file"> <img id"preview" src"">1.2 js代碼如下 // 將上傳的圖片顯示到頁面上function sho…

第一次作業:深入Linux源碼分析進程模型

一.進程的概念 第一&#xff0c;進程是一個實體。每一個進程都有它自己的地址空間&#xff0c;一般情況下&#xff0c;包括文本區域&#xff08;text region&#xff09;、數據區域&#xff08;data region&#xff09;和堆棧&#xff08;stack region&#xff09;。文本區域存…

關于模型驗證那點事兒

今天應笑笑老師之問&#xff0c;做了一個模型驗證的例子&#xff0c;發現之前對這個東西的理解太片面&#xff0c;重新整理了一下思路 字段驗證優先級高于類驗證 什么是類驗證呢&#xff1f;就是兩個字段組合的驗證&#xff0c;比如你Admin不允許修改密碼&#xff0c;你修改密碼…

mongoose --- createUser

說明 源代碼記錄、遺忘回顧mongoDB默認不需要使用賬號密碼即可訪問數據庫.下面是給mongoDB添加超級管理員和普通用戶的方法 以系統管理員的方式運行powershell連接數據庫 mongo查看數據庫: show dbs切換到admin數據庫: use admin創建超級管理員賬戶: db.createUser({user: roo…

Win10安裝MySQL5.7.22 解壓縮版(手動配置)方法

1.下載地址&#xff1a;https://dev.mysql.com/downloads/mysql/5.7.html#downloads 直接點擊下載項 下載后&#xff1a; 2.可以把解壓的內容隨便放到一個目錄&#xff0c;我的是如下目錄&#xff08;放到C盤的話&#xff0c;可能在修改ini文件時涉及權限問題&#xff0c;之后我…

Elemant-UI日期范圍的表單驗證

Form 組件提供了表單驗證的功能&#xff0c;只需要通過 rules 屬性傳入約定的驗證規則&#xff0c;并將 Form-Item 的 prop 屬性設置為需校驗的字段名即可。但是官網的示例只有普通日期類型的驗證&#xff0c;沒有時間范圍的驗證。 一開始&#xff0c;我認為時間時間范圍的是一…

node --- [express項目] 開發環境下使用morgan控制臺輸出訪問信息

說明 源代碼記錄、遺忘回顧 process.env node中提供了一個process.env接口用于訪問計算機中的系統環境變量. 可以利用以上屬性來區分當前的環境是開發環境還是生產環境,代碼如下: if (process.env.NODE_ENV development) {console.log(當前環境是開發環境) } else {consol…

Dynamics CRM 訪問團隊的使用

訪問團隊和負責人團隊的區別是&#xff1a;負責人團隊可以擁有記錄&#xff0c;訪問團隊不能擁有記錄也不能加入解決方案中。 訪問團隊用法1&#xff1a;可以將不同組織的人員加入到訪問組實現數據的更新、刪除、共享 訪問團隊用法2&#xff1a;訪問團隊模板的使用 步驟一&…

業務邏輯

快捷支付接口規范 問題背景 持卡人身份驗證持卡人在發卡銀行提供的身份驗證服務器進行驗證&#xff0c;將結果告知商戶資金清算資金清算在身份驗證通過后進行即時清算&#xff0c;也可能是通過專用資金清算網絡進行傳統方法弊端 持卡人需要訪問很多網站才能完成一次完整支付 &a…

node --- [express] cookie/session 機制與 中間件的使用(路由守衛)

說明 源代碼記憶、遺忘回顧使用 cookie/session 機制,讓 客戶端/服務器 的訪問變得有狀態 cookie 與 session 由于 HTTP 協議的無狀態性,當一次連接斷開后. 服務器并不會記錄用戶是否登錄. 因此需要引入 cookie/session 機制 cookie cookie: 瀏覽器在電腦硬盤中開辟的一塊空…

kprobe原理解析

參考 http://www.cnblogs.com/honpey/p/4575928.html kprobe是linux內核的一個重要特性&#xff0c;是一個輕量級的內核調試工具&#xff0c;同時它又是其他一些更高級的內核調試工具&#xff08;比如perf和systemtap&#xff09;的“基礎設施”&#xff0c;4.0版本的內核中&a…

02 數據類型

轉載于:https://www.cnblogs.com/theoup/p/9875293.html

css --- [學習筆記]背景圖片小結 css三大特性

源代碼 參考 1. 行高(line-height) 目標 理解 - 能說出行高和高度三種關系 - 能簡單理解為什么行高等于單行文字會垂直居應用 使用行高實現單行文字垂直居中能會測量行高 2. CSS 背景(background) 目標 理解 - 背景的作用css 背景圖片和插入圖片的區別 應用 通過 css 背景…

(數據科學學習手札30)樸素貝葉斯分類器的原理詳解Python與R實現

一、簡介 要介紹樸素貝葉斯&#xff08;naive bayes&#xff09;分類器&#xff0c;就不得不先介紹貝葉斯決策論的相關理論&#xff1a; 貝葉斯決策論&#xff08;bayesian decision theory&#xff09;是概率框架下實施決策的基本方法。對分類任務來說&#xff0c;在所有相關概…

【技術累積】【點】【java】【29】MapUtils

內容 是Apache組織下的commons-collections包中的工具類<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency> Map操作相關的&#xff0c…

css --- [讀書筆記] 盒模型(邊框、內外邊距)

說明 源代碼學習 盒子模型(css重點) css學習三大重點: css盒子模型、 浮動、 定位 目標: 能說出盒子模型由哪四部分組成: 內容、邊框、內外邊距能說出內邊距的作用,設置不同數值分別代表的意思: 控制內部塊級元素和寬框的距離能說出塊級盒子居中對齊需要的2個條件能說出外邊…