FFmpeg代碼導(dǎo)讀系列(二)----SEI的那些事

原文首發(fā)在金山云Live,請(qǐng)從原文轉(zhuǎn)載,本文不接受再次轉(zhuǎn)載!

金山云多媒體SDK團(tuán)隊(duì)在移動(dòng)直播、短視頻等項(xiàng)目中遇到了許多FFmpeg問題,特設(shè)立《FFmpeg從入門到出家》系列文稿,希望博君一笑的同時(shí),能讓大家對(duì)FFmpeg有更深入的了解。

流媒體是采用流式傳輸方式在網(wǎng)絡(luò)上播放的媒體格式,視頻網(wǎng)站內(nèi)容、短視頻、在線直播這些視頻形態(tài),均屬于流媒體的不同分支。流媒體大致包含三個(gè)層級(jí):碼流、封裝和協(xié)議。從音視頻編碼器輸出的碼流,經(jīng)過某種封裝格式后,經(jīng)過特定的協(xié)議傳輸、保存,構(gòu)成了流媒體世界的基礎(chǔ)功能。

在直播應(yīng)用的開發(fā)過程中,如果把主播端消息事件傳遞到觀眾端,一般會(huì)以Instant Messaging(即時(shí)通訊)的方式傳遞過去,但因?yàn)橄⒎职l(fā)通道和直播通道是分開的,因此消息與直播音視頻數(shù)據(jù)的同步性就會(huì)出現(xiàn)很多問題。那么有沒有在音視頻內(nèi)部傳遞消息的方法呢?答案是SEI。

1. SEI的介紹

補(bǔ)充增強(qiáng)信息(Supplemental Enhancement Information)是碼流范疇里面的概念,提供了向視頻碼流中加入信息的辦法,是H.264/H.265 視頻壓縮標(biāo)準(zhǔn)的特性之一。SEI 有基本的特征:

  1. 并不是解碼過程的必須項(xiàng);
  2. 有可能對(duì)解碼過程(容錯(cuò)、糾錯(cuò))有幫助;
  3. 集成在視頻碼流中;

這意味著視頻編碼器在輸出視頻碼流的時(shí)候,可以不提供SEI信息 。同時(shí)我們也要清楚:視頻傳輸過程、解封裝、解碼環(huán)節(jié),都可能因?yàn)槟撤N原因丟棄SEI

在視頻內(nèi)容的生成端、傳輸過程中,都可以插入SEI 信息。插入的信息,和其他視頻內(nèi)容一起經(jīng)過傳輸鏈路到達(dá)了消費(fèi)端。那么在SEI 中可以添加哪些信息呢?這里舉幾個(gè)例子,用戶場(chǎng)景可以任意擴(kuò)展:

  1. 傳遞編碼器參數(shù);
  2. 傳遞視頻版權(quán)信息;
  3. 傳遞攝像頭參數(shù);
  4. 傳遞內(nèi)容生成過程中的剪輯事件(引發(fā)場(chǎng)景切換);

1.1 NAL unit類型

網(wǎng)絡(luò)抽象層(Network Abstract Layer)簡稱為NAL。在H.264/AVC視頻編碼標(biāo)準(zhǔn)中,整個(gè)系統(tǒng)框架被分為了兩個(gè)層面:視頻編碼層面(Video Coding Layer - VCL)和網(wǎng)絡(luò)抽象層面(Network Abstraction Layer - NAL)。VCL負(fù)責(zé)表示有效視頻數(shù)據(jù)的內(nèi)容,NAL 負(fù)責(zé)格式化數(shù)據(jù)并提供頭信息,以保證數(shù)據(jù)適合各種信道和存儲(chǔ)介質(zhì)上的傳輸。NAL unitNAL的基本語法結(jié)構(gòu),它包含一個(gè)字節(jié)的頭信息(NAL header)和一系列來自VCL的原始數(shù)據(jù)字節(jié)流(RBSP)。

1.1.1 H.264/AVC 中的情況

NAL unit type儲(chǔ)存在NAL header中,在H.264/AVC標(biāo)準(zhǔn)中,可用的NAL unit type一共有17種,其中值為6時(shí)表征SEI 內(nèi)容。比較常見的類型如下表所示:

NAL unit type NAL unit content
1 非IDR圖像,且不采用數(shù)據(jù)劃分的片段
5 IDR圖像
6 補(bǔ)充增強(qiáng)信息(SEI)
7 序列參數(shù)集(SPS)
8 圖像參數(shù)集(PPS)
11 流結(jié)束符

《ISO/IEC 14496-10:2014》是MPEG專家組為AVC編解碼器制定的標(biāo)準(zhǔn),H.264/AVCNAL unit類型完整定義都在該標(biāo)準(zhǔn)的7-1表中,標(biāo)準(zhǔn)一共預(yù)留了32種類型,所以在NAL header里面,用5 bits表征NAL unit type。

如下圖所示,8 bits的NAL header里面:

  1. 第0位是禁止位0,值為1時(shí)表示語法出錯(cuò);
  2. 第1~2位為參考級(jí)別(NRI,NAL ref idc);
  3. 第3~7為是NAL unit type;
nal_unit_type in H.264/AVC

NRI取值為 "00" (二進(jìn)制)時(shí),表征NAL unit不參與重建參考圖像,這時(shí)的NAL unit是可以丟棄的。大于 "00"(二進(jìn)制)時(shí),NAL unit 不能被丟棄。

1.1.H.265/HEVC 中的情況

《ISO/IEC 23008-2:2015》是MPEG專家組為HEVC編解碼器制定的標(biāo)準(zhǔn),H.265/HEVCNAL unit類型完整定義都在該標(biāo)準(zhǔn)的7-1表中,可用的NAL unit type一共有40種之多,其中39和40都表征SEI內(nèi)容。因?yàn)闃?biāo)準(zhǔn)一共預(yù)留64種類型,所以在NAL header里面,用6 bits表征NAL unit type。

如下圖所示,16 bits的NAL header里面:

  1. 第0位是禁止位0,值為1時(shí)表示語法出錯(cuò);
  2. 第1~6位是NAL unit type;
  3. 第7~12位是NUH layer id
  4. 第13~15位是temporal_id;
nal_unit_type in H.265/HEVC

1.2 SEI 類型

H.264/AVC視頻編碼標(biāo)準(zhǔn)中,并沒有規(guī)定SEI payload type的范圍,所以表征payload type的字節(jié)數(shù)是浮動(dòng)的。
語法分析如下所示,當(dāng)開始解析類型為SEINAL時(shí),持續(xù)讀取8bit,直到非0xff為止,然后把讀取的數(shù)值累加,累加值即為SEI payload type。

sei_message(){
  payloadType = 0
  while( next_bits(8) == 0xFF){
    ff_byte
    payloadType += 255
  }
  last_payload_type_byte
  payloadType += last_payload_type_byte
}

讀取SEI payload sizepayload type邏輯類似,仍然是讀取到0xff為止,這樣可以支持任意長度的SEI payload添加。

sei_message(){
  payloadSize = 0
  while( next_bits(8) == 0xFF){
    ff_byte
    payloadSize += 255
  }
  last_payload_size_byte
  payloadSize += last_payload_size_byte
}

當(dāng)獲取了SEI payload類型和大小后,就進(jìn)入了實(shí)際的SEI內(nèi)容讀取。
當(dāng)前《ISO/IEC 14496-10:2014》Annex D.1.1提供了最大到181的payload類型處理規(guī)范,由于類型可以指定任意大小,給SEI的添加、處理創(chuàng)造了很大的自由空間。
其中SEI payload類型值為5時(shí),指定的處理方法叫user_data_unregistered(),字面含義為未注冊(cè)的用戶數(shù)據(jù),常用于存儲(chǔ)編碼器的編碼參數(shù)信息,是比較常見的payload類型。

讀取payload type為5時(shí),具體的語法解析流程如下:

user_data_unregistered(payloadSize){
  uuid_iso_iec_11578
  for( i=16; i< payloadSize; i++)
    user_data_payload_byte
}

其中uuid_iso_iec_11578的詳細(xì)定義在《
ISO/IEC 11578:1996》Annex A中,大致規(guī)定了使用128bits(16個(gè)字節(jié))來指定UUID。此處UUID可以表征寫入SEI payload的角色I(xiàn)D,或者表征其他業(yè)務(wù)用途。剩下的payloadSize -16字節(jié),即是業(yè)務(wù)層傳遞的具體內(nèi)容了。
通過user_data_unregistered()語法解析可以看出,當(dāng)使用SEI payload type為5時(shí),注意事項(xiàng)如下:

  1. payload size應(yīng)該大于16;
  2. uuid可能出現(xiàn)0x000000/0x000001/0x000002,需要插入0x03做防競(jìng)爭(zhēng)處理;

構(gòu)成RBSP時(shí),都需要做RBSP拖尾處理。拖尾處理對(duì)所有SODB方式都一致。rbsp_trailing_bits()語法邏輯如下:

rbsp_trailing_bits( ){
  rbsp_stop_one_bit
  while( !byte_aligned( ) )
    rbsp_alignment_zero_bit
}

1.3 SEI例子

video.js的示例中下載oceans.mp4并提取出H.264碼流如下:

bitstream from oceans.mp4

NAL header

起始碼(暗紅底色)"0x00000001"分割出來的比特流即是NAL unit,起始碼緊跟的第一個(gè)字節(jié)(墨綠底色)是NAL header。上圖“NAL header”一共出現(xiàn)了四個(gè)數(shù)值:

  • "0x06",此時(shí)NRI為"00B",NAL unit type為SEI類型。
  • “0x67”,此時(shí)NRI為“11B”,NAL unit type為SPS類型。
  • “0x68”,此時(shí)NRI為“11B”,NAL unit type為PPS類型。
  • “0x65”,此時(shí)NRI為“11B”,NAL unit type為IDR圖像。

SEI payload type

"0x06"后一個(gè)字節(jié)為“0x05”(淡黃底色)是SEI payload type,即表征SEI payload分析遵循user_data_unregistered()語法。

SEI payload size

“0x05”后一個(gè)字節(jié)為“0x2F”(淡藍(lán)底色)是SEI payload size,此時(shí)整個(gè)payload是47個(gè)字節(jié)。

SEI payload uuid

"0x2F"隨后的16個(gè)字節(jié)即為uuid,此時(shí)uuid為

dc45e9bd-e6d9-48b7-962c-d820d923eeef

SEI payload content

由于payload size是47個(gè)字節(jié),除去16字節(jié)的uuid,剩下31個(gè)字節(jié)的content。由于content是字符串,所以有結(jié)束符"0x00",有效的30個(gè)字符內(nèi)容是:

Zencoder Video Encoding System

rbsp trailing bits

47個(gè)payload字節(jié)后的"0x80"(灰底色)即是rbsp trailing bits,在user_data_unregistered()里面都是按字節(jié)寫入的,所以此時(shí)的NAL unit結(jié)尾寫入的字節(jié)一定是0x80。

2. SEI的生成

生成SEI的方式很多,大致可以有:

  1. 對(duì)已有碼流做filter,插入SEI NAL;
  2. 視頻編碼時(shí)生成SEI
  3. 容器層寫入時(shí)插入SEI;

以下代碼示例來自于FFmpeg origin/master 分支。

2.1 bsf

BitStream Filter(碼流過濾)的縮寫即為bsf,在不做碼流解碼的前提下,對(duì)已經(jīng)編碼后的比特流做特定的修改、調(diào)整。

bsf h264_metadata的調(diào)用

The ff* tools have a -bsf option applied per stream, taking a comma-separated list of filters, whose parameters follow the filter name after a ’=’.
使用ffmpeg工具時(shí),可以使用比特流過濾器?;镜膄ilter調(diào)用格式如下:

ffmpeg -i INPUT -c:v copy -bsf:v filter1[=opt1=str1:opt2=str2][,filter2] OUTPUT

從上文提到的mp4文件中提取出h.264碼流oceans.h264,可以使用* h264_metadata比特流過濾器添加SEI*。下面示例命令添加了類型為未注冊(cè)的用戶數(shù)據(jù)的SEI,其中uuid為"086f3693-b7b3-4f2c-9653-21492feee5b8",payload內(nèi)容為"hello":

./ffmpeg  -I oceans.h264 -c:v copy -bsf:v h264_metadata=sei_user_data='086f3693-b7b3-4f2c-9653-21492feee5b8+hello' oceans.sei.h264

其中oceans.h264已經(jīng)有一個(gè)SEI和28個(gè)SPS。輸出的oceans.sei.h264碼流中,共有28個(gè)SEI,其中第一個(gè)與輸入保持一致,剩下27個(gè)為新插入的SEI

bsf h264_metadata的代碼分析

具體代碼位于:libavcodec/h264_metadata_bsf.c中。

// 函數(shù)int h264_metadata_filter(AVBSFContext *bsf, AVPacket *out)
if (ctx->sei_user_data && (has_sps || !ctx->sei_first_au)) {
        H264RawSEI *sei;
        H264RawSEIPayload *payload;
        H264RawSEIUserDataUnregistered *udu;
        int sei_pos, sei_new;

        ctx->sei_first_au = 1;

        for (i = 0; i < au->nb_units; i++) {
            if (au->units[i].type == H264_NAL_SEI ||
                au->units[i].type == H264_NAL_SLICE ||
                au->units[i].type == H264_NAL_IDR_SLICE)
                break;
        }
        sei_pos = i;

        if (sei_pos < au->nb_units &&
            au->units[sei_pos].type == H264_NAL_SEI) {
            sei_new = 0;
            sei = au->units[sei_pos].content;
        } else {
            sei_new = 1;
            sei = &ctx->sei_nal;
            memset(sei, 0, sizeof(*sei));
       }
}

以上代碼是h264_metadata添加SEI的判斷邏輯,當(dāng)指定了sei_user_data時(shí),滿足以下條件之一即可以處理:

  • 讀取的access units是第一個(gè)au;
  • 當(dāng)前au包含sps;
    滿足插入SEI邏輯后,具體處理過程中:
  • 如果發(fā)現(xiàn)第一個(gè)NAL已經(jīng)是SEI,則該au不做插入SEI 處理;
  • 如果au包含了IDR幀或者非IDR未分區(qū)的幀,則在其前面插入SEI 信息。

基于以上代碼,oceans.sei.h264碼流中新插入27個(gè)新的SEI 符合處理邏輯。
具體構(gòu)造SEI NAL Unit代碼如下:

        sei->nal_unit_header.nal_unit_type = H264_NAL_SEI;
        err = ff_cbs_insert_unit_content(ctx->cbc, au,
                                             sei_pos, H264_NAL_SEI, sei);
        if (err < 0) {
             av_log(bsf, AV_LOG_ERROR, "Failed to insert SEI.\n");
             goto fail;
        }
        payload = &sei->payload[sei->payload_count];
        payload->payload_type = H264_SEI_TYPE_USER_DATA_UNREGISTERED;
        udu = &payload->payload.user_data_unregistered;
        for (i = j = 0; j < 32 && ctx->sei_user_data[i]; i++) {
            int c, v;
            c = ctx->sei_user_data[i];
            if (c == '-') {
                continue;
            } else if (av_isxdigit(c)) {
                c = av_tolower(c);
                v = (c <= '9' ? c - '0' : c - 'a' + 10);
            } else {
                goto invalid_user_data;
            }
            if (i & 1)
                udu->uuid_iso_iec_11578[j / 2] |= v;
            else
                udu->uuid_iso_iec_11578[j / 2] = v << 4;
            ++j;
        }
        if (j == 32 && ctx->sei_user_data[i] == '+') {
            sei_udu_string = av_strdup(ctx->sei_user_data + i + 1);
            if (!sei_udu_string) {
                err = AVERROR(ENOMEM);
                goto sei_fail;
            }
            udu->data = sei_udu_string;
            udu->data_length = strlen(sei_udu_string);
            payload->payload_size = 16 + udu->data_length;
        }

代碼完整解釋了上文提到的SEI規(guī)范,其中"H264_SEI_TYPE_USER_DATA_UNREGISTERED"值為5,對(duì)應(yīng)的即是未注冊(cè)的用戶信息。在解析"ffmpeg"工具輸入過程中,將"+"號(hào)前面的字符串轉(zhuǎn)換成二進(jìn)制寫入uuid,"+"后內(nèi)容使用字符串寫入payload。

2.2 x264

libx264支持多種SEI類型數(shù)據(jù)寫入,常用的仍然是SEI_USER_DATA_UNREGISTERED,具體的寫入函數(shù)x264_sei_version_write()位于libx264/encoder/set.c中。

int x264_sei_version_write( x264_t *h, bs_t *s )
{
    static const uint8_t uuid[16] =
    {
        0xdc, 0x45, 0xe9, 0xbd, 0xe6, 0xd9, 0x48, 0xb7,
        0x96, 0x2c, 0xd8, 0x20, 0xd9, 0x23, 0xee, 0xef
    };
    char *opts = x264_param2string( &h->param, 0 );
    char *payload;
    int length;

    if( !opts )
        return -1;
    CHECKED_MALLOC( payload, 200 + strlen( opts ) );

    memcpy( payload, uuid, 16 );
    sprintf( payload+16, "x264 - core %d%s - H.264/MPEG-4 AVC codec - "
             "Copy%s 2003-2018 - http://www.videolan.org/x264.html - options: %s",
             X264_BUILD, X264_VERSION, HAVE_GPL?"left":"right", opts );
    length = strlen(payload)+1;

    x264_sei_write( s, (uint8_t *)payload, length, SEI_USER_DATA_UNREGISTERED );

    x264_free( opts );
    x264_free( payload );
    return 0;
fail:
    x264_free( opts );
    return -1;
}

libx264提供的uuid和上文舉例的uuid一致,payload中主要記錄了相關(guān)參數(shù)和版權(quán)信息。以上函數(shù)完成了SEI參數(shù)的構(gòu)造,下面的函數(shù)x264_sei_write完成了具體語法的寫入:

void x264_sei_write( bs_t *s, uint8_t *payload, int payload_size, int payload_type )
{
    int i;

    bs_realign( s );

    for( i = 0; i <= payload_type-255; i += 255 )
        bs_write( s, 8, 255 );
    bs_write( s, 8, payload_type-i );

    for( i = 0; i <= payload_size-255; i += 255 )
        bs_write( s, 8, 255 );
    bs_write( s, 8, payload_size-i );

    for( i = 0; i < payload_size; i++ )
        bs_write( s, 8, payload[i] );

    bs_rbsp_trailing( s );
    bs_flush( s );
}
以上寫入的代碼邏輯和標(biāo)準(zhǔn)語法說明保持一致。

3. SEI的解析

3.1 解析SEI

FFmpeg在讀取和解碼NAL unit,都有相同的邏輯處理SEI
讀取或者解碼數(shù)據(jù)時(shí),會(huì)調(diào)用下面函數(shù)進(jìn)行碼流的解碼,其中buf包含具體的二進(jìn)制流,buf_size是當(dāng)前碼流長度。函數(shù)內(nèi)部會(huì)解析碼流并實(shí)例出具體的NAL對(duì)象:

//Locate in libavcodec/h264dec.c
int decode_nal_units(H264Context *h, const uint8_t *buf, int buf_size)

如果NAL對(duì)象類型是SEI 時(shí),將調(diào)用以下函數(shù)解碼:

//Locate in libavcodec/h264_sei.c
int ff_h264_sei_decode(H264SEIContext *h, GetBitContext *gb,
                       const H264ParamSets *ps, void *logctx)

函數(shù)內(nèi)部會(huì)判斷SEI payload type進(jìn)行不同的函數(shù)調(diào)用,如果是未注冊(cè)的用戶數(shù)據(jù),則調(diào)用以下函數(shù):

 int decode_unregistered_user_data(H264SEIUnregistered *h, GetBitContext *gb,void *logctx, int size)
{
    uint8_t *user_data;
    int e, build, i;

    if (size < 16 || size >= INT_MAX - 16)
        return AVERROR_INVALIDDATA;

    user_data = av_malloc(16 + size + 1);
    if (!user_data)
        return AVERROR(ENOMEM);

    for (i = 0; i < size + 16; i++)
        user_data[i] = get_bits(gb, 8);

    user_data[i] = 0;
    e = sscanf(user_data + 16, "x264 - core %d", &build);
    if (e == 1 && build > 0)
        h->x264_build = build;
    if (e == 1 && build == 1 && !strncmp(user_data+16, "x264 - core 0000", 16))
        h->x264_build = 67;

    av_free(user_data);
    return 0;
}

可以看到,根據(jù)SEI語法標(biāo)準(zhǔn),在解析了SEI payload typelength后,對(duì)未注冊(cè)用戶數(shù)據(jù)的提取,跳過了uuid的分析,只嘗試提取了x264的build信息。總體上,并未利用SEI_USER_DATA_UNREGISTERED傳遞過來的其他相關(guān)參數(shù)信息。
從解碼器邏輯看,H264SEIUnregistered結(jié)構(gòu)體只有一個(gè)x264_build屬性,并未返回實(shí)質(zhì)有效數(shù)據(jù)。上層業(yè)務(wù)如果需要提取SEI_USER_DATA_UNREGISTERED,仍然需要自己提取。提取邏輯,請(qǐng)參考下一小節(jié)(ffplay)。

3.2 ffplay

ffplay是一個(gè)簡單、常用的FFmpeg接口示例工具,常用于測(cè)試解碼、播放效果。如果在ffplay中示例跑通SEI提取功能,可以很方便的移植到其他平臺(tái)。
ffplay工具中,通過av_read_frame(ic, pkt);可以快速拿到當(dāng)前讀到的NAL unit,位于pkt->data中,可以從此取出NAL unit type,如果是SEI且是用戶未注冊(cè)數(shù)據(jù)類型(payload type值為5),則可以參考SEI語法繼續(xù)讀取UUID和其后傳遞的字符串。

4. 總結(jié)

限于篇幅,本文主要對(duì)H.264碼流中涉及用戶未注冊(cè)數(shù)據(jù)的SEI進(jìn)行了分析??傮w而言,SEI只是視頻標(biāo)準(zhǔn)里面很小的一部分,但在應(yīng)用過程中,比如直播問答項(xiàng)目中SEI承載的信息,就極大提升了直播觀看和答題操作的整體用戶體驗(yàn)。所以說,從SEI的例子中,我們就會(huì)發(fā)現(xiàn),視頻標(biāo)準(zhǔn)里面還有很多金礦等待著大家的挖掘,這就是多媒體技術(shù)的魅力。

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

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

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