FFmpeg音視頻解封裝格式

一、什么是封裝格式

封裝格式也稱為容器,用于打包音頻、視頻以及字幕等等,比如常見的容器有 MP4、MOV、WMV、FLV、AVI、MKV 等等。容器里面裝的是音視頻的壓縮幀,但是不是所有類型的壓縮幀都可以裝入容器中,不同的容器對于壓縮幀的格式是有要求的,有一些容器的兼容性要好一些,有一些容器的兼容性就會差一些。

我們平時看到的文件后綴名 mp4 或者 mov 是指文件格式,它的作用是讓我們知道它是何種類型的文件,讓操作系統(tǒng)知道打開文件時改用哪個應(yīng)用打開。正常來講文件后綴名是和封裝格式是有對應(yīng)關(guān)系的,每個容器都有一個或多個文件后綴名。雖然說我們可以隨意修改文件后綴名,但是封裝格式屬于文件的內(nèi)部結(jié)構(gòu),而文件格式是文件外在表現(xiàn),所以修改文件擴展名是無法修改容器原封裝格式的,修改后播放器一般情況下也是可以播放的,因為播放器在播放時會打開文件判斷是哪種容器。

二、使用 FFmpeg 實現(xiàn)解封裝

現(xiàn)在對封裝格式有了一個簡單了解,接下來了解一下封裝格式數(shù)據(jù)是如何被播放出來的,首先要對封裝格式數(shù)據(jù)解封裝,可以得到音頻壓縮數(shù)據(jù)和視頻壓縮數(shù)據(jù),然后再對音頻壓縮數(shù)據(jù)和視頻壓縮數(shù)據(jù)分別進行解碼,就得到了音頻原始數(shù)據(jù)和視頻原始數(shù)據(jù),最后對音頻原始數(shù)據(jù)進行處理送到揚聲器,對視頻數(shù)據(jù)進行處理送到屏幕,并且還要進行音視頻同步處理。本文主要分享的是如何從封裝格式數(shù)據(jù)中拿到音頻原始數(shù)據(jù)和視頻原始數(shù)據(jù),音視頻同步處理先不討論。封裝格式數(shù)據(jù)播放大致實現(xiàn)流程圖如下:

封裝格式數(shù)據(jù)播放流程

下面開始使用 FFmpeg 的 libavformat 庫(它是一個包含用于多媒體容器格式的解復(fù)用器和復(fù)用器的庫)從 MP4 封裝格式中解碼出 YUV 數(shù)據(jù)(原始音頻數(shù)據(jù))和 PCM 數(shù)據(jù)(原始視頻數(shù)據(jù))。

1、創(chuàng)建解封裝上下文打開流媒體文件

int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

參數(shù)說明:
ps:指向解封裝上下文的指針,由 avformat_alloc_context 創(chuàng)建。如果傳 nullptr,函數(shù) avformat_open_input 內(nèi)部會幫我們創(chuàng)建解封裝上下文(注意:函數(shù)調(diào)用失敗時,會釋放開發(fā)者手動創(chuàng)建的解封裝上下文);
url:要打開的流的 url,也就是要打開的流媒體文件(此處也可以傳入設(shè)備名稱和設(shè)備序號,我們在音視頻錄制時傳入的就是設(shè)備序號);
fmt:如果非 nulllptr 將使用特定的輸入格式,傳 nullptr 將自動檢測輸入格式;
options:包含解封裝上下文和解封裝器特有的參數(shù)的字典。
最后不要忘記使用函數(shù) avformat_close_input 關(guān)閉解封裝上下文,使用函數(shù) avformat_close_input 就不需要再調(diào)用函數(shù) avformat_free_context 了,其內(nèi)部幫我們調(diào)用了函數(shù) avformat_free_context

// 源碼片段 ffmpeg-4.3.2/libavformat/utils.c
int avformat_open_input(AVFormatContext **ps, const char *filename,
                        ff_const59 AVInputFormat *fmt, AVDictionary **options)
{
    AVFormatContext *s = *ps;
    int i, ret = 0;
    AVDictionary *tmp = NULL;
    ID3v2ExtraMeta *id3v2_extra_meta = NULL;

    if (!s && !(s = avformat_alloc_context()))
        return AVERROR(ENOMEM);
    if (!s->av_class) {
        av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
        return AVERROR(EINVAL);
    }
    if (fmt)
        s->iformat = fmt;

    if (options)
        av_dict_copy(&tmp, *options, 0);

    if (s->pb) // must be before any goto fail
        s->flags |= AVFMT_FLAG_CUSTOM_IO;

    if ((ret = av_opt_set_dict(s, &tmp)) < 0)
        goto fail;

    // 省略代碼... 

     if (options) {
        av_dict_free(options);
        *options = tmp;
    }
    *ps = s;
    return 0;

close:
    if (s->iformat->read_close)
        s->iformat->read_close(s);
fail:
    ff_id3v2_free_extra_meta(&id3v2_extra_meta);
    av_dict_free(&tmp);
    if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
        avio_closep(&s->pb);
    avformat_free_context(s);
    *ps = NULL;
    return ret;
}

2、檢索流信息

2.1、檢索流信息

該函數(shù)可以讀取一部分音視頻數(shù)據(jù)并且獲得一些相關(guān)的信息:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

參數(shù)說明:
ic:需要讀取信息的解封裝上下文;
options:額外一些參數(shù)。

2.2、導(dǎo)出流信息到控制臺

我們可以使用下面函數(shù)打印檢索到的詳細信息到控制臺,包括音頻流的采樣率、通道數(shù)等,視頻流包括視頻的 width、height、pixel format、碼率、幀率等信息:

void av_dump_format(AVFormatContext *ic,
                    int index,
                    const char *url,
                    int is_output);

ic:需要打印分析的解封裝上下文;
index:需要導(dǎo)出信息的流索引;
url:需要打印的輸入或者輸出流媒體文件 url。
is_output:是否輸出,0 = 輸入 / 1 = 輸出;

在 Qt 中還需要調(diào)用 fflush(stderr) 才能夠?qū)⑿畔⑤敵龅娇刂婆_。fflush 會強迫將緩沖區(qū)內(nèi)容清空,就會立即輸出所有在緩沖區(qū)中的內(nèi)容。stderr 是指標準錯誤輸出設(shè)備,輸出的文本內(nèi)容一般是紅色的,默認向屏幕輸出內(nèi)容。

打印的信息如下,這和我們在終端看到的信息是一樣的:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/mac/Downloads/pic/in.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    title           : www.lggzs.com
    encoder         : Lavf58.45.100
  Duration: 00:00:10.04, bitrate: N/A
    Stream #0:0(und): Video: h264 (avc1 / 0x31637661), none, 640x480, 355 kb/s, SAR 1:1 DAR 4:3, 23.98 fps, 23.98 tbr, 24k tbn (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 48000 Hz, 2 channels, 129 kb/s (default)
    Metadata:
      handler_name    : SoundHandler

3、初始化音頻解碼器查找合適的音視流和視頻流信息

讀取多媒體文件音頻流和視頻流信息,函數(shù) av_find_best_stream 是在 FFmpeg 新版本中添加的,老版本只可通過遍歷的方式讀取,我們可以通過 stream->codecpar->codec_type 判斷流類型,可以取得同樣的效果:

int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);

參數(shù)說明:
ic:需要處理的流媒體文件,解封裝上下文中包含流媒體文件信息;
type:要檢索的流類型,比如音頻流、視頻流和字幕流等等;
wanted_stream_nb:請求的流序號,傳 -1 自動選擇;
related_stream:查找相關(guān)流,不查找傳 -1;
decoder_ret:返回當(dāng)前流對應(yīng)的解碼器。函數(shù)調(diào)用成功,并且參數(shù) decoder_ret 不為 nullptr,將通過參數(shù) decoder_ret 返回一個對應(yīng)的解碼器;
flags:目前沒有定義;

流類型枚舉:

enum AVMediaType {
    AVMEDIA_TYPE_UNKNOWN = -1,  ///< Usually treated as AVMEDIA_TYPE_DATA
    AVMEDIA_TYPE_VIDEO,
    AVMEDIA_TYPE_AUDIO,
    AVMEDIA_TYPE_DATA,          ///< Opaque data information usually continuous
    AVMEDIA_TYPE_SUBTITLE,
    AVMEDIA_TYPE_ATTACHMENT,    ///< Opaque data information usually sparse
    AVMEDIA_TYPE_NB
};

函數(shù)調(diào)用成功返回流序號,如果位找到請求類型的流返回 AVERROR_STREAM_NOT_FOUND,如果找到了請求的流但是沒有對應(yīng)的解碼器將返回 AVERROR_DECODER_NOT_FOUND。

4、檢驗流

我們成功的查找到流后最好要檢驗一下流是否真的存在;

AVStream *stream = _fmtCtx->streams[streamIdx];
if (!stream) {
    qDebug() << "audio / video streams is empty.";
    return -1;
}

5、查找解碼器

我們通過 stream->codecpar->codec_id 可以查找到對應(yīng)的解碼器:

AVCodec *avcodec_find_decoder(enum AVCodecID id);

5、創(chuàng)建解碼上下文

創(chuàng)建解碼上下文,需要傳遞上面查找到的解碼器(也可以不傳,但解碼上下文不會包含解碼器):

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

參數(shù)說明:
codec:解碼器;

最后需要使用函數(shù) avcodec_free_context 釋放解碼上下文。

6、拷貝流參數(shù)到解碼器

在 FFmpeg 舊版本中保存流信息參數(shù)是 AVStream 結(jié)構(gòu)體中的 codec 字段。新版本中已經(jīng)將 AVStream 結(jié)構(gòu)體中的 codec 字段定義為廢棄屬性。因此無法像以前舊版本中直接通過參數(shù) codec 獲取流信息。當(dāng)前版本保存流信息的參數(shù)是 AVStream 結(jié)構(gòu)體中的 codecpar 字段,F(xiàn)Fmpeg 提供了函數(shù) avcodec_parameters_to_context 將流信息拷貝到新的解碼器中:

int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

參數(shù)說明:
codec:解碼器;
par:流中的參數(shù),通過 stream->codecpar 獲?。?/p>

6、打開解碼器

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

參數(shù)說明:
avctx:需要初始化的解碼上下文;
codec:解碼器;
options:包含解封裝上下文和解封裝器特有的參數(shù)的字典。

7、從音視頻流中讀取壓縮幀

我們可以通過 pkt->stream_index 判斷讀取到的壓縮幀是音頻壓縮幀還是視頻壓縮幀等等,然后分別對音視頻壓縮數(shù)據(jù)進行解碼:

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

參數(shù)說明:
s:解封裝上下文;
pkt:讀取到的壓縮幀數(shù)據(jù);

在調(diào)用函數(shù) avcodec_send_packet 之前我們需要創(chuàng)建一個 AVPacket。在 FFmpeg 版本 4.4 中 av_init_packet 函數(shù)已經(jīng)過期,實際上 FFmpeg 不建議我們把 AVPacket 放到棧空間了。建議使用函數(shù) av_packet_alloc 來創(chuàng)建,av_packet_alloc 創(chuàng)建的 AVPacket 是在堆空間的。下面寫法不提倡:

// pkt 是在函數(shù)中定義,pkt 內(nèi)存在??臻g,所以 pkt 內(nèi)存不需要我們?nèi)ド暾埡歪尫?AVPacket pkt;
// init 僅僅是初始化,并不會分配內(nèi)存
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;

最后需要使用函數(shù) av_packet_free 釋放 AVPacket,注意函數(shù) av_packet_unref 僅僅是把 AVPacket 指向的一些額外內(nèi)存釋放掉,并不會釋放 AVPacket 內(nèi)存空間。

8、音視頻解碼

首先使用函數(shù) avcodec_send_packet 發(fā)送壓縮數(shù)據(jù)到解碼器:

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

然后使用函數(shù) avcodec_receive_frame 從解碼器中讀取解碼后的數(shù)據(jù):

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

9、保存音視頻輸出參數(shù)

我們定義了兩個結(jié)構(gòu)體分別保存音頻輸出參數(shù)和視頻輸出參數(shù):

// 音頻輸出參數(shù)
typedef struct {
    const char *filename; // 文件名
    int sampleRate; // 采樣率
    AVSampleFormat sampleFmt; // 采樣格式
    int chLayout; // 聲道布局
} AudioDecodeSpec;

// 視頻輸出參數(shù)
typedef struct {
    const char *filename; // 文件名
    int width; // 寬
    int height; // 高
    AVPixelFormat pixFmt; // 像素格式
    int fps; // 幀率
} VideoDecodeSpec;

保存音頻參數(shù):

_aOut->sampleRate = _aDecodeCtx->sample_rate;
_aOut->sampleFmt = _aDecodeCtx->sample_fmt;
_aOut->chLayout = _aDecodeCtx->channel_layout;

保存視頻參數(shù):

_vOut->width = _vDecodeCtx->width;
_vOut->height = _vDecodeCtx->height;
_vOut->pixFmt = _vDecodeCtx->pix_fmt;
_vOut->fps = _vDecodeCtx->framerate.num / _vDecodeCtx->framerate.den;

通過上面方法獲取到的幀率有可能是 0,我們需要使用函數(shù) av_guess_frame_rate 獲取幀率:

AVRational framerate = av_guess_frame_rate(_fmtCtx, _fmtCtx->streams[_vStreamIdx], nullptr);
_vOut->fps = framerate.num / framerate.den;

10、音視頻原始數(shù)據(jù)寫入文件

10.1、音頻原始數(shù)據(jù)寫入文件

我們的最終目的是將音頻原始數(shù)據(jù)寫入到 PCM 文件并使用 ffplay 命令進行播放。因為播放器是不支持播放 planar 格式數(shù)據(jù)的,所以要求寫入文件的數(shù)據(jù)為非 planar 格式。我們可以通過函數(shù) av_sample_fmt_is_planar 來判斷當(dāng)前音頻原始數(shù)據(jù)是否為 planar 格式,對于非 planar 格式數(shù)據(jù),我們要把每個聲道中的音頻樣本交錯寫入文件。非 planar 格式直接寫入文件即可:

立體聲 Planar 格式音頻原始數(shù)據(jù)寫入 PCM 文件

void Demuxer::writeAudioFrame()
{
    if (av_sample_fmt_is_planar(_aDecodeCtx->sample_fmt)) { // planar
        for (int si = 0; si < _frame->nb_samples; si++) {
            for (int ci = 0; ci < _aDecodeCtx->channels; ci++) {
                uint8_t *begin = (uint8_t *)(_frame->data[ci] + _sampleSize * si);
                _aOutFile->write((char *)begin, _sampleSize);
            }
        }
    } else { // non-planar
        _aOutFile->write((char *)_frame->data[0], * frame-> nb_samples * _sampleFrameSize);
    }
}

函數(shù) av_sample_fmt_is_planar 內(nèi)部會去 sample_fmt_info 表中查詢當(dāng)前采樣格式是否為 planar 格式:

// 源碼片段 ffmpeg-4.3.2/libavutil/samplefmt.c
/** this table gives more information about formats */
static const SampleFmtInfo sample_fmt_info[AV_SAMPLE_FMT_NB] = {
    [AV_SAMPLE_FMT_U8]   = { .name =   "u8", .bits =  8, .planar = 0, .altform = AV_SAMPLE_FMT_U8P  },
    [AV_SAMPLE_FMT_S16]  = { .name =  "s16", .bits = 16, .planar = 0, .altform = AV_SAMPLE_FMT_S16P },
    [AV_SAMPLE_FMT_S32]  = { .name =  "s32", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_S32P },
    [AV_SAMPLE_FMT_S64]  = { .name =  "s64", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_S64P },
    [AV_SAMPLE_FMT_FLT]  = { .name =  "flt", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_FLTP },
    [AV_SAMPLE_FMT_DBL]  = { .name =  "dbl", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_DBLP },
    [AV_SAMPLE_FMT_U8P]  = { .name =  "u8p", .bits =  8, .planar = 1, .altform = AV_SAMPLE_FMT_U8   },
    [AV_SAMPLE_FMT_S16P] = { .name = "s16p", .bits = 16, .planar = 1, .altform = AV_SAMPLE_FMT_S16  },
    [AV_SAMPLE_FMT_S32P] = { .name = "s32p", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_S32  },
    [AV_SAMPLE_FMT_S64P] = { .name = "s64p", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_S64  },
    [AV_SAMPLE_FMT_FLTP] = { .name = "fltp", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_FLT  },
    [AV_SAMPLE_FMT_DBLP] = { .name = "dblp", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_DBL  },
};

在音頻中,planar 格式每個聲道的大小都是一樣的,所以只有 frame->linesize[0] 有值,frame->linesize[1] 是沒有值的。linesize 是指緩沖區(qū)大小,有可能 frame 中的樣本數(shù)量并不足以填滿緩沖區(qū),所以在寫入文件時,寫入文件數(shù)據(jù)大小需要使用下面方式計算,函數(shù) av_get_bytes_per_sample 獲取到的是每個樣本所占字節(jié)數(shù),再乘以聲道數(shù),就得到了每個音頻樣本幀的大小:

// _sampleSize 每個音頻樣本的大小
_sampleSize = av_get_bytes_per_sample(aDecodeCtx->sampleFmt);
// _sampleFrameSize 每個音頻樣本幀的大小
_sampleFrameSize = sampleSize * _aDecodeCtx->channels;

10.2、視頻原始數(shù)據(jù)寫入文件

// 創(chuàng)建視頻原始數(shù)據(jù)緩沖區(qū),為了兼容多種原始數(shù)據(jù)格式
_imageSize = av_image_alloc(_imageBuf, _imageLinesize, _vDecodeCtx->width, _vDecodeCtx->height, _vDecodeCtx->pix_fmt, 1);
void Demuxer::writeVideoFrame()
{
    // 拷貝 frame 中數(shù)據(jù)到 _imageBuf
    av_image_copy(_imageBuf, _imageLinesize, (const uint8_t **)(_frame->data), _frame->linesize, _vDecodeCtx->pix_fmt, _vDecodeCtx->width, _vDecodeCtx->height);
    //qDebug() << _imageBuf << _imageSize;
    _vOutFile->write((char *)_imageBuf[0], _imageSize);
}

11、關(guān)閉文件 & 釋放資源

_aOutFile->close();
_vOutFile->close();
avcodec_free_context(&_aDecodeCtx);
avcodec_free_context(&_vDecodeCtx);
avformat_close_input(&_fmtCtx);
av_packet_free(&_pkt);
av_frame_free(&_frame);
av_freep(&_imgBuf[0]);
三、使用 FFmpeg 命令行解封裝
$ ffmpeg -c:v h264 -c:a aac -i in.mp4 out_terminal.yuv -f f32le out_terminal.pcm

和使用 FFmpeg 命令行生成的 PCM 和 YUV 文件大小對比:

$ ls -al
-rw-r--r--   1 mac  staff          614562 Apr 20 12:38 in.mp4
-rw-r--r--   1 mac  staff         3850240 Apr 20 16:25 out_code.pcm
-rw-r--r--   1 mac  staff       109670400 Apr 20 16:25 out_code.yuv
-rw-r--r--   1 mac  staff         3850240 Apr 20 16:18 out_terminal.pcm
-rw-r--r--   1 mac  staff       110592000 Apr 20 16:18 out_terminal.yuv

通過對比發(fā)現(xiàn)使用代碼得到的 yuv 文件丟失了部分數(shù)據(jù),通過排查發(fā)現(xiàn)是由于最后忘記刷新解碼器緩沖區(qū)導(dǎo)致的,刷新解碼器數(shù)據(jù)緩沖區(qū)后代碼和命令行生成的 PCM 和 YUV 文件大小完全一樣。

四、總結(jié)

初始化解碼器的流程(紅框中)音頻和視頻都是一樣的,僅僅 AVMediaType 不同,解碼流程(綠框中)也是一樣的。這部分代碼音視頻是可以共用的。整體流程參考下圖:

流程總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容