Qt與FFmpeg聯合開發指南(二)-- 解碼本地視頻

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

一、開發前的準備工作(詳見上章節)

開發工具爲Qt5.10.1,目錄結構:

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

二、編解碼基礎知識

(1)封裝格式

所謂封裝格式是指音視頻的組合格式,例如最常見的封裝格式有mp4、mp3、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數據,包括計算畫面的寬高等。

但是音頻則不然,音頻的播放必須和採集保持同步。提高或降低音頻的播放速度都會讓音質發生變化,這也是變聲器的原理。因此在實際開發中爲了保證播放的音視頻同步,我們往往會按照音頻的播放速度來控制視頻的解碼轉碼速度。

( 4 ) 框架圖

 

三、代碼實現

第一步:註冊所有組件
av_register_all();

第二步:打開視頻輸入文件

    QString filename = QCoreApplication::applicationDirPath();
    qDebug()<<"獲取程序運行目錄 "<<filename;
    char* cinputFilePath = "屌絲男士.mov";  //本地視頻文件放入程序運行目錄
     AVFormatContext* avformat_context = avformat_alloc_context();
    //參數一:封裝格式上下文->AVFormatContext->包含了視頻信息(視頻格式、大小等等...)
    //參數二:打開文件(入口文件)->url
    int avformat_open_result = avformat_open_input(&avformat_context,cinputFilePath,NULL,NULL);
    if (avformat_open_result != 0)
    {
        //獲取異常信息
        char* error_info = new char[32];
        av_strerror(avformat_open_result, error_info, 1024);
        qDebug()<<QString("異常信息 %1").arg(avformat_open_result);
        return false;
    };
    
第三步:查找視頻流信息
    //參數一:封裝格式上下文->AVFormatContext
    //參數二:配置
    //返回值:0>=返回OK,否則失敗
    int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
    if (avformat_find_stream_info_result < 0){
        //獲取失敗
        char* error_info = new char[32];
        av_strerror(avformat_find_stream_info_result, error_info, 1024);
        qDebug()<<QString("異常信息 %1").arg(error_info);
        return false;
    }
第四步:查找解碼器
    //第一點:獲取當前解碼器是屬於什麼類型解碼器->找到了視頻流
    //音頻解碼器、視頻解碼器、字幕解碼器等等...
    //獲取視頻解碼器流引用
    int av_stream_index = -1;
    for (int i = 0; i < avformat_context->nb_streams; ++i) {
        //循環遍歷每一流
        //視頻流、音頻流、字幕流等等...
        if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到了
            av_stream_index = i;
            break;
        }
    }
    if (av_stream_index == -1){
        qDebug()<<QString("沒有找到視頻流");
        return false;
    }
    //第二點:根據視頻流->查找到視頻解碼器上下文->視頻壓縮數據
    AVCodecContext* avcodec_context = avformat_context->streams[av_stream_index]->codec;

    //第三點:根據解碼器上下文->獲取解碼器ID
    AVCodec* avcodec = avcodec_find_decoder(avcodec_context->codec_id);
    if (avcodec == NULL)
    {
        qDebug()<<QString("沒有找到視頻解碼器");
        return false;
    }
第五步:打開解碼器
    int avcodec_open2_result = avcodec_open2(avcodec_context,avcodec,NULL);
    if (avcodec_open2_result != 0)
    {
        char* error_info = new char[32];
        av_strerror(avformat_find_stream_info_result, error_info, 1024);
        qDebug()<<QString("異常信息 %1").arg(error_info);
        return false;
    }

 以上基本就是打開多媒體文件的主要步驟,解碼和轉碼的所有參數都可以在這裏獲取。接下來我們就需要循環進行讀取、解碼、轉碼直到播放完成。

可以使用以下方式進行打印

    qDebug()<<QString("文件格式: %1").arg(avformat_context->iformat->name);
    //輸出:解碼器名稱
    qDebug()<<QString("解碼器名稱: %1").arg(avcodec->name);
    qDebug()<<QString("寬 %1 高 %2").arg(avcodec_context->width).arg(avcodec_context->height);
    //此函數自動打印輸入或輸出的詳細信息
    av_dump_format(avformat_context, 0, cinputFilePath, 0);

 如下圖所示(可以和MediaInfo軟件進行對照查看):

 

 

第六步:循環讀取視頻幀,進行循環解碼,轉碼輸出YUV420P視頻->格式:yuv格式

解碼流程:

    //讀取幀數據換成到哪裏->緩存到packet裏面
    AVPacket* av_packet = (AVPacket*)av_malloc(sizeof(AVPacket));
    //輸入->環境一幀數據->緩衝區->類似於一張圖
    AVFrame* av_frame_in = av_frame_alloc();
    //輸出->幀數據->視頻像素數據格式->yuv420p
    AVFrame* av_frame_out_yuv420p = av_frame_alloc();

    //緩衝區分配內存
    uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, avcodec_context->width, avcodec_context->height));
    //初始化緩衝區
    avpicture_fill((AVPicture *)av_frame_out_yuv420p, out_buffer, AV_PIX_FMT_YUV420P, avcodec_context->width, avcodec_context->height);

    //解碼的狀態類型(0:表示解碼完畢,非0:表示正在解碼)
    int  av_decode_result, y_size, u_size, v_size, current_frame_index = 0;

    SwsContext* sws_context = sws_getContext(avcodec_context->width,
                                             avcodec_context->height,
                                             avcodec_context->pix_fmt,
                                             avcodec_context->width,
                                             avcodec_context->height,
                                             AV_PIX_FMT_YUV420P,
                                             SWS_BICUBIC,NULL,NULL,NULL);

    //打開文件
    FILE* out_file_yuv = fopen("diao.yuv","wb+");
    if (out_file_yuv == NULL){
        qDebug()<<QString("文件不存在 ");
        return false;
    }


    //>=0:說明有數據,繼續讀取   <0:說明讀取完畢,結束
    while (av_read_frame(avformat_context,av_packet) >= 0){
        //解碼什麼類型流(視頻流、音頻流、字幕流等等...)
        if (av_packet->stream_index == av_stream_index){

            //發送一幀數據
            avcodec_send_packet(avcodec_context, av_packet);
            //接收一幀數據->解碼一幀
            av_decode_result = avcodec_receive_frame(avcodec_context, av_frame_in);
            //解碼出來的每一幀數據成功之後,將每一幀數據保存爲YUV420格式文件類型(.yuv文件格式)
            if ( av_decode_result == 0 ){
            
                sws_scale(sws_context,
                          (const uint8_t *const*)av_frame_in->data,
                          av_frame_in->linesize,
                          0,
                          avcodec_context->height,
                          av_frame_out_yuv420p->data,
                          av_frame_out_yuv420p->linesize);
  
                //yuv420規則一:Y結構表示一個像素點
                //yuv420規則二:四個Y對應一個U和一個V(也就是四個像素點,對應一個U和一個V)
                // y = 寬 * 高
                // u = y / 4
                // v = y / 4
                y_size = avcodec_context->width * avcodec_context->height;
                u_size = y_size / 4;
                v_size = y_size / 4;

                //寫入->Y
                //av_frame_in->data[0]:表示Y
                fwrite(av_frame_in->data[0], 1, y_size, out_file_yuv);
                //寫入->U
                //av_frame_in->data[1]:表示U
                fwrite(av_frame_in->data[1], 1, u_size, out_file_yuv);
                //寫入->V
                //av_frame_in->data[2]:表示V
                fwrite(av_frame_in->data[2], 1, v_size, out_file_yuv);

                current_frame_index++;

                qDebug()<<QString("當前遍歷第 %1 幀").arg(current_frame_index);


            }

        }
    }
第七步:關閉解碼組件
    av_packet_free(&av_packet);
    //關閉流
    fclose(out_file_yuv);
    av_frame_free(&av_frame_in);
    av_frame_free(&av_frame_out_yuv420p);
    avcodec_close(avcodec_context);
    avformat_free_context(avformat_context);

解碼後存儲地yuv視頻文件使用:YUV Player Deluxe 軟件進行查看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章