FFmpeg編解碼處理1-轉碼全流程簡介

本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-odtaqzoo-cr.htmlhtml

FFmpeg編解碼處理系列筆記:
[0]. FFmpeg時間戳詳解
[1]. FFmpeg編解碼處理1-轉碼全流程簡介
[2]. FFmpeg編解碼處理2-編解碼API詳解
[3]. FFmpeg編解碼處理3-視頻編碼
[4]. FFmpeg編解碼處理4-音頻編碼git

基於 FFmpeg 4.1 版本。github

1. 轉碼全流程簡介

看一下 FFmpeg 常規處理流程:
FFmpeg處理流程shell

大流程能夠劃分爲輸入、輸出、轉碼、播放四大塊。其中轉碼涉及比較多的處理環節,從圖中能夠看出,轉碼功能在整個功能圖中佔比很大。轉碼的核心功能在解碼和編碼兩個部分,但在一個可用的示例程序中,編碼解碼與輸入輸出是難以分割的。解複用器爲解碼器提供輸入,解碼器會輸出原始幀,對原始幀可進行各類複雜的濾鏡處理,濾鏡處理後的幀經編碼器生成編碼幀,多路流的編碼幀經複用器輸出到輸出文件。ide

1.1 解複用

從輸入文件中讀取編碼幀,判斷流類型,根據流類型將編碼幀送入視頻解碼器或音頻解碼器。svn

av_read_frame(ictx.fmt_ctx, &ipacket);
if (codec_type == AVMEDIA_TYPE_VIDEO) {
    transcode_video(&stream, &ipacket);
} else if (codec_type == AVMEDIA_TYPE_AUDIO) {
    transcode_audio(&stream, &ipacket);
}
else {
    av_interleaved_write_frame(octx.fmt_ctx, &ipacket);
}

1.2 解碼

將視音頻編碼幀解碼生成原始幀。後文詳述。函數

1.3 濾鏡

FFmpeg 提供多種多樣的濾鏡,用來處理原始幀數據。佈局

本例中,爲每一個音頻流/視頻流使用空濾鏡,即濾鏡圖中將 buffer 濾鏡和 buffersink 濾鏡直接相連。目的是:經過視頻 buffersink 濾鏡將視頻流輸出像素格式轉換爲編碼器採用的像素格式;經過音頻 abuffersink 濾鏡將音頻流輸出聲道佈局轉換爲編碼器採用的聲道佈局。爲下一步的編碼操做做好準備。若是不使用這種方法,則須要處理圖像格式轉換和音頻重採樣,從而確保進入編碼器的幀是編碼器支持的格式。測試

固然,例程可擴展,能夠很容易的在 buffer 濾鏡和 buffersink 濾鏡中間插入其餘功能濾鏡,實現豐富的視音頻處理功能。優化

濾鏡的使用方法不是本實驗關注的重點。詳細用法可參考 "FFmpeg原始幀處理-濾鏡API用法

1.4 編碼

將原始視音頻幀編碼生成編碼幀。後文詳述。

1.5 複用

將編碼幀按不一樣流類型交織寫入輸出文件。

av_interleaved_write_frame(octx.fmt_ctx, &ipacket);

2. 轉碼例程簡介

轉碼功能複雜,示例程序很難寫得簡短,這幾篇筆記共用同一份示例代碼。在 shell 中運行以下命令下載例程源碼:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_transcode

例程支持在命令行中指定視音頻編碼格式以及輸出文件封裝格式。若是編碼格式指定爲 "copy",則輸出流使用與輸入流相同的編碼格式。與 FFmpeg 命令不一樣的是,FFmpeg 命令指定編碼器參數爲 "copy" 時,將不會啓動編解碼過程,而僅啓用轉封裝過程,整個過程很快執行完畢;本例程指定編碼格式爲 "copy" 時,則會使用相同的編碼格式進行解碼與編碼,整個過程比較耗時。

例程驗證方法:

./transcode -i input.flv -c:v mpeg2video -c:a mp2 output.ts

和以下命令效果大體同樣:

ffmpeg -i input.flv -c:v mpeg2video -c:a mp2 output.ts

源代碼文件說明:

Makefile
main.c          轉複用轉碼功能
av_codec.c      編碼解碼功能
av_filter.c     濾鏡處理
open_file.c     打開輸入輸出文件

轉碼的主流程主要在 main. c中 transcode_video()、transcode_audio() 和 transcode_audio_with_afifo() 三個函數中。當輸入音頻幀尺寸能被音頻編碼器接受時,使用 transcode_audio() 函數;不然,引入音頻 FIFO,使每次從 FIFO 中取出的音頻幀尺寸能被音頻編碼器接受,使用 transcode_audio_with_afifo() 函數實現此功能。這幾個函數僅提供示意功能,演示音視頻轉碼功能的實現方法,源碼糾結、可讀性差,暫無時間優化。

2.1 視頻轉碼流程

視頻轉碼函數 transcode_video(),其主要處理流程以下(已刪除大量細節代碼):

static int transcode_video(const stream_ctx_t *sctx, AVPacket *ipacket)
{
    AVFrame *frame_dec = av_frame_alloc();
    AVFrame *frame_flt = av_frame_alloc();
    AVPacket opacket;

    // 一個視頻packet只包含一個視頻frame,但沖洗解碼器時一個flush packet會取出
    // 多個frame出來,每次循環取處理一個frame
    while (1)   
    {
        // 1. 時間基轉換,解碼
        av_packet_rescale_ts(ipacket, sctx->i_stream->time_base, sctx->o_codec_ctx->time_base);
        ret = av_decode_frame(sctx->i_codec_ctx, ipacket, &new_packet, frame_dec);

        // 2. 濾鏡處理
        ret = filtering_frame(sctx->flt_ctx, frame_dec, frame_flt);

        // 3. 編碼
        // 3.1 設置幀類型
        frame_flt->pict_type = AV_PICTURE_TYPE_NONE;
        // 3.2 編碼
        ret = av_encode_frame(sctx->o_codec_ctx, frame_flt, &opacket);
        // 3.3 更新編碼幀中流序號
        opacket.stream_index = sctx->stream_idx;
        // 3.4 時間基轉換,AVPacket.pts和AVPacket.dts的單位是AVStream.time_base,不一樣的封裝格式其
        //     AVStream.time_base不一樣因此輸出文件中,每一個packet須要根據輸出封裝格式從新計算pts和dts
        av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base);

        // 4. 將編碼後的packet寫入輸出媒體文件
        ret = av_interleaved_write_frame(sctx->o_fmt_ctx, &opacket);
        av_packet_unref(&opacket);
    }

    return ret;
}

2.2 音頻轉碼流程

音頻轉碼函數 transcode_audio(),其主要處理流程以下(已刪除大量細節代碼):

static int transcode_audio_with_afifo(const stream_ctx_t *sctx, AVPacket *ipacket)
{
    AVFrame *frame_dec = av_frame_alloc();
    AVFrame *frame_flt = av_frame_alloc();
    AVFrame *frame_enc = NULL;
    AVPacket opacket;
    int enc_frame_size = sctx->o_codec_ctx->frame_size;
    AVAudioFifo* p_fifo = sctx->aud_fifo;
    static int s_pts = 0;
    
    while (1)   // 處理一個packet,一個音頻packet可能包含多個音頻frame,循環每次處理一個frame
    {
        // 1. 時間基轉換,解碼
        av_packet_rescale_ts(ipacket, sctx->i_stream->time_base, sctx->o_codec_ctx->time_base);
        ret = av_decode_frame(sctx->i_codec_ctx, ipacket, &new_packet, frame_dec);

        // 2. 濾鏡處理
        ret = filtering_frame(sctx->flt_ctx, frame_dec, frame_flt);

        // 3. 使用音頻fifo,從而保證每次送入編碼器的音頻幀尺寸知足編碼器要求
        // 3.1 將音頻幀寫入fifo,音頻幀尺寸是解碼格式中音頻幀尺寸
        if (!dec_finished)
        {
            uint8_t** new_data = frame_flt->extended_data;  // 本幀中多個聲道音頻數據
            int new_size = frame_flt->nb_samples;           // 本幀中單個聲道的採樣點數
            
            // FIFO中可讀數據小於編碼器幀尺寸,則繼續往FIFO中寫數據
            ret = write_frame_to_audio_fifo(p_fifo, new_data, new_size);
        }

        // 3.2 從fifo中取出音頻幀,音頻幀尺寸是編碼格式中音頻幀尺寸
        // FIFO中可讀數據大於編碼器幀尺寸,則從FIFO中讀走數據進行處理
        while ((av_audio_fifo_size(p_fifo) >= enc_frame_size) || dec_finished)
        {
            // 從FIFO中讀取數據,編碼,寫入輸出文件
            ret = read_frame_from_audio_fifo(p_fifo, sctx->o_codec_ctx, &frame_enc);

            // 4. fifo中讀取的音頻幀沒有時間戳信息,從新生成pts
            frame_enc->pts = s_pts;
            s_pts += ret;

flush_encoder:
            // 5. 編碼
            ret = av_encode_frame(sctx->o_codec_ctx, frame_enc, &opacket);

            // 5.1 更新編碼幀中流序號,並進行時間基轉換
            //     AVPacket.pts和AVPacket.dts的單位是AVStream.time_base,不一樣的封裝格式其AVStream.time_base不一樣
            //     因此輸出文件中,每一個packet須要根據輸出封裝格式從新計算pts和dts
            opacket.stream_index = sctx->stream_idx;
            av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base);

            // 6. 將編碼後的packet寫入輸出媒體文件
            ret = av_interleaved_write_frame(sctx->o_fmt_ctx, &opacket);
        }

        if (finished)
        {
            break;
        }
    }

    return ret;
}

2.3 轉碼過程當中的時間戳處理

在封裝格式處理例程中,不深刻理解時間戳也沒有關係。但在編解碼處理例程中,時間戳處理是很重要的一個細節,必需要搞清楚。

容器(文件層)中的時間基(AVStream.time_base)與編解碼器上下文(視頻層)裏的時間基(AVCodecContex.time_base)不同,解碼編碼過程當中須要進行時間基轉換。

視頻按幀進行播放,因此原始視頻幀時間基爲 1/framerate。視頻解碼前須要處理輸入 AVPacket 中各時間參數,將輸入容器中的時間基轉換爲 1/framerate 時間基;視頻編碼後再處理輸出 AVPacket 中各時間參數,將 1/framerate 時間基轉換爲輸出容器中的時間基。

音頻按採樣點進行播放,因此原始音頻幀時間爲 1/sample_rate。音頻解碼前須要處理輸入 AVPacket 中各時間參數,將輸入容器中的時間基轉換爲 1/sample_rate 時間基;音頻編碼後再處理輸出 AVPacket 中各時間參數,將 1/sample_rate 時間基轉換爲輸出容器中的時間基。若是引入音頻 FIFO,從 FIFO 從讀出的音頻幀時間戳信息會丟失,須要使用 1/sample_rate 時間基從新爲每個音頻幀生成 pts,而後再送入編碼器。

解碼前的時間基轉換:

av_packet_rescale_ts(ipacket, sctx->i_stream->time_base, sctx->o_codec_ctx->time_base);

編碼後的時間基轉換:

av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base);

關於時間基與時間戳的詳細內容可參考 "FFmpeg時間戳詳解"。編解碼過程主要關注音視頻幀的 pts,用戶可不關注 dts,詳細說明可參考 "FFmpeg編解碼處理3-編解碼API詳解"。

3. 編譯與驗證

在 shell 中運行以下命令下載例程源碼:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_transcode

在源碼目錄執行 make 命令,生成 transcode 可執行文件

下載測試文件(右鍵另存爲):tnmil2.flv
迷龍

使用 ffprobe 看一下文件格式:

think@opensuse> ffprobe tnmil2.flv 
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, flv, from 'tnmil2.flv':
  Metadata:
    encoder         : Lavf58.20.100
  Duration: 00:00:13.68, start: 0.057000, bitrate: 474 kb/s
    Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s

使用輸入文件中的編碼格式和封裝格式生成輸出文件

./transcode -i tnmil2.flv -c:v copy -c:a copy tnmil2o.flv

指定編碼格式和封裝格式生成輸出文件

./transcode -i tnmil2.flv -c:v mpeg2video -c:a mp2 tnmil2.ts

7. 參考資料

[1]. FFmpeg關於nb_smples,frame_size以及profile的解釋https://blog.csdn.net/zhuweigangzwg/article/details/53335941
[2]. What does the output of ffmpeg mean? tbr tbn tbc etc?
[3]. 視頻編解碼基礎概念, http://www.javashuo.com/article/p-kfylstit-o.html
[4]. 對ffmpeg的時間戳的理解筆記, https://blog.csdn.net/topsluo/article/details/76239136
[6]. ffmpeg中的時間戳與時間基, http://www.imooc.com/article/91381
[6]. ffmpeg編解碼中涉及到的pts詳解, http://www.52ffmpeg.com/article/353.html
[7]. 音視頻錄入的pts和dts問題, http://www.javashuo.com/article/p-sfpidpug-ko.html

8. 修改記錄

2019-03-23 V1.0 初稿

相關文章
相關標籤/搜索