Qt與FFmpeg聯合開發指南(一)——解碼(1):功能實現

前言:對於從未接觸過音視頻編解碼的同窗來講,使用FFmpeg的學習曲線恐怕略顯陡峭。本人因爲工做須要,正好須要在項目中使用。所以特意將開發過程總結下來。只當提供給有興趣的同窗參考和學習。git

因爲FFmpeg是使用C語言開發,全部和函數調用都是面向過程的。以我目前的學習經驗來講,一般我會把一個功能的代碼所有放在main函數中實現。通過測試和修改認爲功能正常,再以C++面向對象的方式逐步將代碼分解和封裝。所以在對本套指南中我也會採用先代碼實現再功能封裝的步驟。算法

1、開發前的準備工做網絡

開發工具爲VS2013+Qt5,目錄結構:ide

  • bin:工做和測試目錄
  • doc:開發文檔目錄
  • include:ffmpeg頭文件配置目錄
  • lib:ffmpeg靜態庫配置目錄
  • src:源碼目錄

屬性頁配置:函數

  1. 常規-輸出目錄:..\..\bin
  2. 調試-工做目錄:..\..\bin
  3. C/C++-常規-附加包含目錄:..\..\include
  4. 連接器-常規-附加庫目錄:..\..\lib
  5. 連接器-系統-子系統:控制檯 (/SUBSYSTEM:CONSOLE)

2、編解碼基礎知識工具

(1)封裝格式性能

所謂封裝格式是指音視頻的組合格式,例如最多見的封裝格式有mp四、mp三、flv等。簡單來講,咱們平時接觸到的帶有後綴的音視頻文件都是一種封裝格式。不一樣的封裝格式遵循不一樣的協議標準。有興趣的同窗能夠自行擴展,更深的東西我也不懂。學習

(2)編碼格式開發工具

以mp4爲例,一般應該包含有視頻和音頻。視頻的編碼格式爲YUV420P,音頻的編碼格式爲PCM。再以YUV420編碼格式爲例。咱們知道一般圖像的顯示爲RGB(紅綠藍三原色),在視頻壓縮的時候會首先將表明每一幀畫面的RGB壓縮爲YUV,再按照關鍵幀(I幀),過渡幀(P幀或B幀)進行運算和編碼。解碼的過程正好相反,解碼器會讀到I幀,並根據I幀運算和解碼P幀以及B幀。並最終根據視頻文件預設的FPS還原每一幀畫面的RGB數據。最後推送給顯卡。因此一般咱們說的編碼過程就包括:畫面採集、轉碼、編碼再封裝。測試

(3)視頻解碼和音頻解碼有什麼區別

玩遊戲的同窗確定對FPS不陌生,FPS過低畫面會感受閃爍不夠連貫,FPS越高須要顯卡性能越好。一些高速攝像機的採集速度可以達到11000幀/秒,那麼在播放這類影片的時候咱們是否也須要以11000幀/秒播放呢?固然不是,一般咱們會按照25幀/秒或者60幀/秒設定圖像的FPS值。可是因爲視頻存在關鍵幀和過渡幀的區別,關鍵幀保存了完整的畫面而過渡幀只是保存了與前一幀畫面的變化部分,須要經過關鍵幀計算得到。所以咱們須要對每一幀都進行解碼,即獲取畫面的YUV數據。同時只對咱們真正須要顯示的畫面進行轉碼,即將YUV數據轉換成RGB數據,包括計算畫面的寬高等。

可是音頻則否則,音頻的播放必須和採集保持同步。提升或下降音頻的播放速度都會讓音質發生變化,這也是變聲器的原理。所以在實際開發中爲了保證播放的音視頻同步,咱們每每會按照音頻的播放速度來控制視頻的解碼轉碼速度。

3、代碼實現

(1)註冊FFmpeg組件:註冊和初始化FFmpeg封裝器和網絡設備

av_register_all();
avformat_network_init();
avdevice_register_all();

 (2)打開文件和建立輸入設備

AVFormatContext *pFormatCtx = NULL;
int errnum = avformat_open_input(&pFormatCtx, filename, NULL, NULL);
if (errnum < 0) {
    av_strerror(errnum, errbuf, sizeof(errbuf));
    cout << errbuf << endl;
}

AVFormatContext 表示一個封裝器,在讀取多媒體文件的時候,它負責保存與封裝和編解碼有關的上下文信息。avformat_open_input函數能夠根據文件後綴名來建立封裝器。

(3)遍歷流並初始化解碼器

for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
    AVCodecContext *pCodecCtx = pFormatCtx->streams[i]->codec; // 解碼器上下文
    if (pCodecCtx->codec_type == AVMEDIA_TYPE_VIDEO) { // 視頻通道
        int videoIndex = i;
            
        // 視頻的寬,高
        int srcWidth = pCodecCtx->width;
        int srcHeight = pCodecCtx->height;
            
        // 建立視頻解碼器,打開解碼器
        AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
        if (!codec) {
            // 沒法建立對應的解碼器
        }

        errnum = avcodec_open2(pCodecCtx, codec, NULL);
        if (errnum < 0) {
            av_strerror(errnum, errbuf, sizeof(errbuf));
            cout << errbuf << endl;
        }
        cout << "video decoder open success!" << endl;
    }
    if (pCodecCtx->codec_type == AVMEDIA_TYPE_AUDIO) { // 音頻通道
        int audioIndex = i;
        // 建立音頻解碼器,打開解碼器
        AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
        if (!codec) {
            // 沒法建立對應的解碼器
        }

        errnum = avcodec_open2(pCodecCtx, codec, NULL);
        if (errnum < 0) {
            av_strerror(errnum, errbuf, sizeof(errbuf));
            cout << errbuf << endl;
        }

        int sampleRate = pCodecCtx->sample_rate; // 音頻採樣率
        int channels = pCodecCtx->channels; // 聲道數
        AVSampleFormat fmt = pCodecCtx->sample_fmt; // 樣本格式

        cout << "audio decoder open success!" << endl;
    }
}    

封裝器中保存了各類流媒體的通道,一般視頻通道爲0,音頻通道爲1。除此之外可能還包含字幕流通道等。

第2步和第3步基本就是打開多媒體文件的主要步驟,解碼和轉碼的全部參數均可以在這裏獲取。接下來咱們就須要循環進行讀取、解碼、轉碼直到播放完成。

(4)讀取壓縮數據:之因此稱爲壓縮數據主要是爲了區分AVPacketAVFrame兩個結構體。AVPacket表示一幅通過了關鍵幀或過渡幀編碼後的畫面,AVFrame表示一個AVPacket通過解碼後的完整YUV畫面

AVPacket *pkt = NULL;
pkt = av_packet_alloc(); // 初始化AVPacket
// 讀取一幀數據
errnum = av_read_frame(pFormatCtx, pkt);
if (errnum == AVERROR_EOF) {
    // 已經讀取到文件尾
    av_strerror(errnum, errbuf, sizeof(errbuf));
    cout << errbuf << endl;
}
if (errnum < 0) {
    av_strerror(errnum, errbuf, sizeof(errbuf));
    cout << errbuf << endl;
}

(5)解碼

errnum = avcodec_send_packet(pCodecCtx, pkt);
if (errnum < 0) {
    av_strerror(errnum, errbuf, sizeof(errbuf));
    cout << errbuf << endl;
}

AVFrame *yuv = av_frame_alloc();
AVFrame *pcm = av_frame_alloc();
if (pkt->stream_index == videoIndex) { // 判斷當前解碼幀爲視頻幀
    errnum = avcodec_receive_frame(pCodecCtx, yuv); // 解碼視頻
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        cout << errbuf << endl;
    }
}
if (pkt->stream_index == audioIndex) { // 判斷當前解碼幀爲音頻幀
    errnum = avcodec_receive_frame(pCodecCtx, pcm); // 解碼音頻
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        cout << errbuf << endl;
    }
}

(6)視頻轉碼

// 720p輸出標準
int outWidth = 720;
int outHeight = 480;
char *outData = new char[outWidth * outHeight * 4]

SwsContext *videoSwsCtx = NULL;
videoSwsCtx = sws_getCachedContext(videoSwsCtx, srcWidth, srcHeight, (AVPixelFormat)pixFmt, // 輸入
    outWidth, outHeight, AV_PIX_FMT_BGRA, // 輸出
    SWS_BICUBIC, // 算法
    0, 0, 0);

// 分配數據空間
uint8_t *dstData[AV_NUM_DATA_POINTERS] = { 0 };
dstData[0] = (uint8_t *)outData;
int dstStride[AV_NUM_DATA_POINTERS] = { 0 };
dstStride[0] = outWidth * 4;

int h = sws_scale(videoSwsCtx, yuv->data, yuv->linesize, 0, srcHeight, dstData, dstStride);
if (h != outHeight) {
    // 轉碼失敗
}

這裏須要解釋一下outWidth * outHeight * 4計算理由:720p標準的視頻畫面包含720 * 480個像素點,每個像素點包含了RGBA4類數據,每一類數據分別由1個byte即8個bit表示。所以一幅完整畫面所佔的大小爲outWidth * outHeight * 4。

(7)音頻轉碼

char *outData = new char[10000]; 輸出指針

AVCodecContext *pCodecCtx = pFormatCtx->streams[audioIndex]->codec; // 獲取音頻解碼器上下文
SwrContext *audioSwrCtx = NULL;
audioSwrCtx = swr_alloc();
audioSwrCtx = swr_alloc_set_opts(audioSwrCtx,
    AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, // 輸出參數:雙通道立體聲 CD音質
    pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate, // 輸入參數
    0, 0);
swr_init(audioSwrCtx);

uint8_t *out[AV_NUM_DATA_POINTERS] = { 0 };
out[0] = (uint8_t *)outData;
// 計算輸出空間
int dst_nb_samples = av_rescale_rnd(pcm->nb_samples, pCodecCtx->sample_rate, pCodecCtx->sample_rate, AV_ROUND_UP);
int len = swr_convert(audioSwrCtx,
    out, dst_nb_samples,
    (const uint8_t **)pcm->data, pcm->nb_samples);
    
int channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO); // AV_CH_LAYOUT_STEREO -> 2 根據聲道類型獲得聲道數
// 實際音頻數據長度
int dst_bufsize = av_samples_get_buffer_size(NULL,
    channels, // 通道數
    pcm->nb_samples,// 1024
    AV_SAMPLE_FMT_S16,
    0);
if (dst_bufsize < 0) {
    // 音頻轉碼錯誤
}

至此咱們已經基本完成了對一個多媒體文件的解碼工做,不過離真正的播放還有一些工做沒有完成。包括對代碼的封裝和界面設計咱們都會放在下一篇博客中介紹。

完整的項目代碼:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player

相關文章
相關標籤/搜索