32.Android Studio下FFmpeg的編譯和使用(六.FFmpeg音視頻解碼)

項目源碼
解碼分爲軟解碼和硬解碼,那麼什麼是軟解碼和硬解碼,二者有什麼區別?簡單來說,在於是否使用CPU進行解碼,最初視頻解碼都是通過CPU進行的,那時候視頻分辨率較低,CPU完全可以勝任解碼的工作,但是隨着高清視頻的出現,使用CPU進行解碼的壓力越來越大

軟解碼

使用CPU進行解碼,所以就很容易造成CPU負載過大。純粹依靠CPU來解碼,是在顯卡本身不支持或者部分不支持硬件解碼的前提下,將解壓高清編碼的任務交給CPU,這是基於硬件配置本身達不到硬解壓要求的前提下的無奈之舉。我們在解碼過程中,應該在硬件支持的前提下優先使用硬解碼。
對於一個超級電視而言,觀看高清電影無疑是用戶最大的訴求,而硬解碼的優勢就在於可以流暢的支持1080p甚至4K清晰度的電影播放,而不需要佔用CPU,CPU就可以如釋重負,輕鬆上陣,承擔更多的其他任務。如果通過軟解碼的方式播放高清電影,CPU的負擔較重,往往會出現卡頓、不流暢的現象。

硬解碼

硬件解碼就是通過顯卡的視頻加速功能對高清視頻進行解碼,使用非CPU進行,如GPU/VPU(GPU:圖形處理器:Graphics Processing Unit,指計算機的顯卡,VPU:Visual Processing Unit,視覺處理單元,由ATI提出的、用於區別於傳統GPU的概念,實際二者均爲顯示處理核心,本質上並無任何區別)、專用的DSP、FPGA、ASIC芯片等,所以幾乎不會佔用CPU,部分產品在GPU硬件平臺移植了優秀的軟編碼算法(如X264),解碼質量基本等同於軟編碼。硬解碼可以將CPU從繁重的視頻解碼中解放出來,使播放設備具備流暢播放高清視頻的能力。顯卡的GPU/VPU要比CPU更適合這類大數據量的、低難度的重複工作。

那麼ffmpeg是如何進行軟解碼和硬解碼的,分別來看

解碼步驟

使用ffmpeg解碼分爲如下幾步:
1.找到解碼器
avcodec_find_decoder(軟) avcodec_find_decoder_by_name(硬)
2.獲取解碼器上下文
avcodec_alloc_context3
3.填充解碼器參數到上下文中
avcodec_parameters_to_context
4.打開解碼器
avcodec_open2
5.將解封裝得到的AVPacket發送到解碼器中進行解碼
avcodec_send_packet
6.從解碼器中接收已經解碼完成的數據存入AVFrame
avcodec_receive_frame
7.釋放frame空間
av_frame_unref

具體到代碼中看一看音視頻的軟解碼和硬解碼

視頻軟解碼環境
    /***************************************video解碼器*********************************************/
    //找到視頻解碼器(軟解碼)
    AVCodec *videoAVCodec = avcodec_find_decoder(avFormatContext->streams[videoIndex]->codecpar->codec_id);
    avcodec_open2 video failed!
    if (videoAVCodec == NULL){
        LOGE("avcodec_find_decoder failed !");
        return;
    }
    //初始化視頻解碼器上下文對象
    AVCodecContext *videoCodecContext = avcodec_alloc_context3(videoAVCodec);
    //根據所提供的編解碼器的值填充編解碼器上下文參數
    avcodec_parameters_to_context(videoCodecContext,avFormatContext->streams[videoIndex]->codecpar);
    //設置視頻解碼器解碼的線程數,解碼時將會以你設定的線程進行解碼
    videoCodecContext->thread_count = 8;
    //打開解碼器
    result = avcodec_open2(videoCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 video failed! %s",av_err2str(result));
        return;
    }
    /***********************************************************************************************/
視頻硬解碼環境
    //硬解碼,硬解碼需要Jni_OnLoad中做設置否則ffmpeg_player_error: avcodec_open2 video failed!
    AVCodec *videoAVCodec = avcodec_find_decoder_by_name("h264_mediacodec");
    if (videoAVCodec == NULL){
        LOGE("avcodec_find_decoder failed !");
        return;
    }
    //初始化視頻解碼器上下文對象
    AVCodecContext *videoCodecContext = avcodec_alloc_context3(videoAVCodec);
    //根據所提供的編解碼器的值填充編解碼器上下文參數
    avcodec_parameters_to_context(videoCodecContext,avFormatContext->streams[videoIndex]->codecpar);
    //設置視頻解碼器解碼的線程數,解碼時將會以你設定的線程進行解碼
    videoCodecContext->thread_count = 8;
    //打開解碼器
    result = avcodec_open2(videoCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 video failed! %s",av_err2str(result));
        return;
    }

可以看到軟硬解碼的區別也只是在於獲取解碼器的那一步操作

音頻軟解碼環境
    /***************************************audio解碼器*********************************************/

    //找到音頻解碼器(軟解碼)
    AVCodec *audioAVCodec = avcodec_find_decoder(avFormatContext->streams[audioIndex]->codecpar->codec_id);
    //初始化音頻解碼器上下文對象
    AVCodecContext *audioCodecContext = avcodec_alloc_context3(audioAVCodec);
    //根據所提供的編解碼器的值填充編解碼器上下文參數
    avcodec_parameters_to_context(audioCodecContext,avFormatContext->streams[audioIndex]->codecpar);
    //設置音頻解碼器解碼的線程數,解碼時將會以你設定的線程進行解碼
    audioCodecContext->thread_count = 1;
    //打開音頻解碼器
    result = avcodec_open2(audioCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 audio failed!");
        return;
    }
    /***********************************************************************************************/
音頻硬解碼環境
/***************************************audio解碼器*********************************************/

    //找到音頻解碼器(軟解碼)
    //硬解碼
    AVCodec *audioAVCodec = avcodec_find_decoder_by_name("h264_mediacodec");
    //初始化音頻解碼器上下文對象
    AVCodecContext *audioCodecContext = avcodec_alloc_context3(audioAVCodec);
    //根據所提供的編解碼器的值填充編解碼器上下文參數
    avcodec_parameters_to_context(audioCodecContext,avFormatContext->streams[audioIndex]->codecpar);
    //設置音頻解碼器解碼的線程數,解碼時將會以你設定的線程進行解碼
    audioCodecContext->thread_count = 1;
    //打開音頻解碼器
    result = avcodec_open2(audioCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 audio failed!");
        return;
    }
    /***********************************************************************************************/

同樣也是區別在於解碼器的獲取

獲取到解碼器之後解碼的過程代碼如下:

    for (;;) {

        //********************測試每秒解碼幀數代碼*******************
        if(GetNowMs() - start >=3000){
            LOGI("now decode fps is %d",frameCount/3);
            start = GetNowMs();
            frameCount = 0;
        }
        //********************測試每秒解碼幀數代碼*******************

        //Return the next frame of a stream.
        int read_result = av_read_frame(avFormatContext,avPacket);
        if(read_result != 0){
            //讀取到結尾處,從20秒位置繼續開始播放
            LOGI("讀取到結尾處 %s",av_err2str(read_result));
            //跳轉到指定的position播放,最後一個參數表示
            //int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
            //av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
            //LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
            //continue;
            break;
        }
        LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
             avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
        );

        //解碼測試
        if(avPacket->stream_index != videoIndex){
            continue;
        }

        AVCodecContext *codecContext = videoCodecContext;
        if (avPacket->stream_index == audioIndex){
            codecContext = audioCodecContext;
        }

        //將packet發送到解碼器中進行解碼
        read_result = avcodec_send_packet(videoCodecContext,avPacket);
        if (read_result != 0){
            LOGE("avcodec_send_packet failed!");
            continue;
        }

        for(;;){
            //從解碼器中返回的已經解碼的數據
            read_result = avcodec_receive_frame(codecContext,avFrame);
            if(read_result != 0){
                LOGE("avcodec_receive_frame failed!");
                break;
            }

            //********************測試每秒解碼幀數代碼*******************
            //說明解碼的是視頻,
            if(codecContext == videoCodecContext) {
                frameCount++;
            }
            //********************測試每秒解碼幀數代碼*******************

            LOGW("avcodec_receive_frame %lld",avFrame->pts);
        }

        //packet使用完成之後執行,否則內存會急劇增長
        //不再引用這個packet指向的空間,並且將packet置爲default狀態
        av_packet_unref(avPacket);
    }
軟硬解碼已經多線程解碼效率測試結果

neon 單線程下解碼視頻結果:
每秒幀數27~68
CPU佔用16%左右
內存佔用67M左右

neon 八線程下解碼視頻結果
每秒解碼幀數100~170幀
CPU佔用70%左右
內存佔用90M左右

neon h264_mediacodec硬解碼 設置單線程
每秒解碼幀數46~58,硬解碼是一個固定值,這是由於計算誤差產生的
CPU佔用3%左右
內存佔用23M左右

neon h264_mediacodec硬解碼 設置8線程
每秒解碼幀數46~85,線程數對幀率無影響,因爲硬解碼幀率是固定的
CPU和內存的佔用可忽略不計

總結一下

1.軟解碼相較於硬解碼,對內存和CPU的開銷都明顯很大
2.硬解碼的解碼幀率是固定的,但是幾乎不佔用CPU

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