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

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

1、解碼速度算法

播放器播放媒體文件的時候播放進度須要咱們本身控制。基本的控制方法有兩種:設計模式

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

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

方案一函數

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作一次換算。算法以下:spa

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

2、封裝思路討論線程

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

基本上,若是咱們只須要設計一個簡單的播放器。大概須要三個模塊的支持:代理

界面模塊(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或PCM
    int toRGB(char *outData, int outWidth, int outHeight);
    int toPCM(char *outData);
    
    int durationMsec; // 文件時長
    int fps; // 視頻FPS
    int 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

相關文章
相關標籤/搜索