日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

FFmpeg从入门到精通:SEI那些事

發布時間:2024/4/11 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 FFmpeg从入门到精通:SEI那些事 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文是“FFmpeg從入門到精通”系列的第三篇,由金山云供稿,并授權LiveVideoStack發布。此前兩篇為FFmpeg代碼導讀——基礎篇和FFmpeg代碼導讀——HEVC在RTMP中的擴展。FFmpeg廣泛應用與音視頻領域,被譽為音視頻開發的“瑞士軍刀”。“FFmpeg從入門到精通”系列將由淺入深,解讀FFmpeg的基礎功能與使用技巧。


文 / 阿曾


流媒體是采用流式傳輸方式在網絡上播放的媒體格式,視頻網站內容、短視頻、在線直播這些視頻形態,均屬于流媒體的不同分支。流媒體大致包含三個層級:碼流、封裝和協議。從音視頻編碼器輸出的碼流,經過某種封裝格式后,經過特定的協議傳輸、保存,構成了流媒體世界的基礎功能。


在直播應用的開發過程中,如果把主播端消息事件傳遞到觀眾端,一般會以Instant Messaging(即時通訊)的方式傳遞過去,但因為消息分發通道和直播通道是分開的,因此消息與直播音視頻數據的同步性就會出現很多問題。那么有沒有在音視頻內部傳遞消息的方法呢?答案是SEI。金山云目前推出的直播問答解決方案中,就用到了SEI,作為一名視頻云架構資深開發工程師,今天就來和大家分享一下SEI的技術細節。


SEI即補充增強信息(Supplemental Enhancement Information),屬于碼流范疇,它提供了向視頻碼流中加入額外信息的方法,是H.264/H.265這些視頻壓縮標準的特性之一。SEI的基本特征如下:


  • 并非解碼過程的必須選項

  • 可能對解碼過程(容錯、糾錯)有幫助

  • 集成在視頻碼流中


  • 也就是說,視頻編碼器在輸出視頻碼流的時候,可以不提供SEI信息。雖然在視頻的傳輸過程、解封裝、解碼這些環節,都可能因為某種原因丟棄SEI內容,但在視頻內容的生成端和傳輸過程中,是可以插入SEI信息的。這些插入的信息,和其他視頻內容一同經過傳輸鏈路到達消費端。舉例來說,當前火爆的直播問答模式,就是通過SEI傳遞較多和答題業務相關的信息,通過SEI承載的信息,極大地優化了題目顯示和觀眾音視頻觀看的同步性。


    那么在SEI中可以添加哪些信息呢?以下是一些用戶場景可任意擴展的例子:


  • 傳遞編碼器參數

  • 傳遞視頻版權信息

  • 傳遞攝像頭參數

  • 傳遞內容生成過程中的剪輯事件(引發場景切換)


  • 對于SEI如何應用,我們先以H.264/AVC這一視頻編碼標準為例。在這一標準中,整個系統框架分為兩層:視頻編碼層面(Video Coding Layer,簡稱VCL)和網絡抽象層面(Network Abstraction Layer,簡稱NAL)。VCL負責表示有效視頻數據的內容,NAL負責格式化數據并提供頭信息,以保證數據適合各種信道和存儲介質上的傳輸。NAL unit是NAL的基本語法結構,它包含一個字節的頭信息(NAL header)和一系列來自VCL的原始數據字節流(RBSP)。


    H.264/AVC中的情況


    NAL unit type儲存在NAL header中,在H.264/AVC標準中,可用的NAL unit type一共有17種,作用是告訴解碼器,承載的數據是視頻關鍵幀,還是視頻解碼器的配置參數信息。其中值為6時表征SEI內容。比較常見的類型如下表所示:


    NAL unit type NAL unit content



    H.264/AVC中完整的NAL unit類型定義請參考《ISO/IEC 14496-10:2014》,這是MPEG專家組為AVC編解碼器制定的標準,H.264/AVC中NAL unit類型完整定義都在該標準的7-1表中,標準一共預留了32種類型,在NAL header里面,用5 bits表征NAL unit type。


    H.264/AVC中的NAL unit type


    如上圖所示,在8 bits的NAL header中:


  • 第0位是禁止位0,值為1時表示語法出錯

  • 第1~2位是參考級別(NRI,NAL ref idc)

  • 第3~7位是NAL unit type


  • 需要注意的是,當NRI取值為"00"(二進制)時,表征NAL unit不參與重建參考圖像,這時的NAL unit是可以丟棄的。大于"00"(二進制)時,NAL unit不能被丟棄。


    H.265/HEVC中的情況


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


    H.265/HEVC中的NAL unit type


    如上圖所示,在16 bits的NAL header中:


  • 第0位是禁止位0,值為1時表示語法出錯

  • 第1~6位是NAL unit type

  • 第7~12位是NUH layer id

  • 第13~15位是temporal_id


  • SEI 類型


    在H.264/AVC視頻編碼標準中,并沒有規定SEI payload type的范圍,所以表征payload type的字節數是浮動的。


    語法分析如下所示,當開始解析類型為SEI的NAL時,持續讀取8bit,直到非0xff為止,然后把讀取的數值累加,累加值即為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 size和payload 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
    }


    當獲取了SEI payload類型和大小后,就進入了實際的SEI內容讀取。


    當前《ISO/IEC 14496-10:2014》Annex D.1.1提供了最大到181的payload類型處理規范,由于類型可以指定任意大小,給SEI的添加、處理創造了很大的自由空間。


    其中SEI payload類型值為5時,指定的處理方法叫user_data_unregistered(),字面含義為未注冊的用戶數據,常用于存儲編碼器的編碼參數信息,是比較常見的payload類型。


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


    function DisplayWindowSize(){

    ?var w=window.innerWidth
    ?|| document.documentElement.clientWidth
    ?|| document.body.clientWidth;
    }


    其中uuid_iso_iec_11578的詳細定義在《ISO/IEC 11578:1996》 Annex A中,大致規定了使用128 bits(16個字節)來指定UUID。此處UUID可以表征寫入SEI payload的角色ID,或者表征其他業務用途。剩下的payloadSize -16字節,即是業務層傳遞的具體內容了。


    通過user_data_unregistered()語法解析可以看出,當使用SEI payload type為5時,注意事項如下:


  • payload size應該大于16;

  • uuid可能出現0x000000/0x000001/0x000002,需要插入0x03做防競爭處理;


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


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


    SEI例子


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


    bitstream from oceans.mp4


    NAL header


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


    • "0x06",此時NRI為"00B",NAL unit type為SEI類型。

    • “0x67”,此時NRI為“11B”,NAL unit type為SPS類型。

    • “0x68”,此時NRI為“11B”,NAL unit type為PPS類型。

    • “0x65”,此時NRI為“11B”,NAL unit type為IDR圖像。


    SEI payload type


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


    SEI payload size


    “0x05”后一個字節為“0x2F”(淡藍底色)是SEI payload size,此時整個payload是47個字節。


    SEI payload uuid


    "0x2F"隨后的16個字節即為uuid,此時uuid為


    dc45e9bd-e6d9-48b7-962c-d820d923eeef


    SEI payload content


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


    Zencoder Video Encoding System


    rbsp trailing bits


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


    SEI的生成


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


  • 對已有碼流做filter,插入SEI NAL

  • 視頻編碼時生成SEI

  • 容器層寫入時插入SEI


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


    Bsf


    BitStream Filter(碼流過濾)的縮寫為bsf,它的作用是,在不做碼流解碼的前提下,對已經編碼后的比特流做特定的修改、調整。


    bsf h264_metadata的調用


    使用ffmpeg工具時,可以使用比特流過濾器。基本的filter調用格式如下:


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


    從上文提到的mp4文件中提取出h.264碼流oceans.h264,可以使用 h264_metadata比特流過濾器添加SEI。下面示例命令添加了類型為未注冊的用戶數據的SEI,其中uuid為"086f3693-b7b3-4f2c-9653-21492feee5b8",payload內容為"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已經有一個SEI和28個SPS。輸出的oceans.sei.h264碼流中,共有28個SEI,其中第一個與輸入保持一致,剩下27個為新插入的SEI。


    bsf h264_metadata的代碼分析


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


    // 函數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的判斷邏輯,當指定了sei_user_data時,滿足以下條件之一即可以處理:


    • 讀取的access units是第一個au;

    • 當前au包含sps;


    滿足插入SEI邏輯后,具體處理過程中:


    • 如果發現第一個NAL已經是SEI,則該au不做插入SEI處理;

    • 如果au包含了IDR幀或者非IDR未分區的幀,則在其前面插入SEI信息。


    基于以上代碼,oceans.sei.h264碼流中新插入27個新的SEI 符合處理邏輯。


    具體構造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規范,其中"H264_SEI_TYPE_USER_DATA_UNREGISTERED"值為5,對應的即是未注冊的用戶信息。在解析"ffmpeg"工具輸入過程中,將"+"號前面的字符串轉換成二進制寫入uuid,"+"后內容使用字符串寫入payload。


    x264


    libx264支持多種SEI類型數據寫入,常用的仍然是SEI_USER_DATA_UNREGISTERED,具體的寫入函數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中主要記錄了相關參數和版權信息。以上函數完成了SEI參數的構造,下面的函數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 );
    }


    以上寫入的代碼邏輯和標準語法說明保持一致。


    解析SEI


    FFmpeg在讀取和解碼NAL unit,都有相同的邏輯處理SEI。


    讀取或者解碼數據時,會調用下面函數進行碼流的解碼,其中buf包含具體的二進制流,buf_size是當前碼流長度。函數內部會解析碼流并實例出具體的NAL對象:


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


    如果NAL對象類型是SEI 時,將調用以下函數解碼:


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


    函數內部會判斷SEI payload type進行不同的函數調用,如果是未注冊的用戶數據,則調用以下函數:


    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;
    }


    可以看到,根據SEI語法標準,在解析了SEI payload type和length后,對未注冊用戶數據的提取,跳過了uuid的分析,只嘗試提取了x264的build信息。總體上,并未利用SEI_USER_DATA_UNREGISTERED傳遞過來的其他相關參數信息。


    從解碼器邏輯看,H264SEIUnregistered結構體只有一個x264_build屬性,并未返回實質有效數據。上層業務如果需要提取SEI_USER_DATA_UNREGISTERED,仍然需要自己提取。提取邏輯,請參考下一小節(ffplay)。


    ffplay


    ffplay是一個簡單、常用的FFmpeg接口示例工具,常用于測試解碼、播放效果。如果在ffplay中示例跑通SEI提取功能,可以很方便的移植到其他平臺。


    在ffplay中通過函數av_read_frame(ic, pkt)返回后,讀取pkt->data可以快速拿到當前讀到的NAL unit。從data數據中取出NAL unit type,如果是SEI且是用戶未注冊數據類型(payload type值為5),則可以參考SEI語法繼續讀取UUID和其后傳遞的字符串。


    本文主要對H.264碼流中涉及用戶未注冊數據的SEI進行了分析。總體而言,SEI只是視頻標準里面很小的一部分,但在應用過程中,比如直播問答項目中SEI承載的信息,就極大提升了直播觀看和答題操作的整體用戶體驗。所以說,從SEI的例子中,我們就會發現,視頻標準里面還有很多金礦等待著大家的挖掘,這就是多媒體技術的魅力,也是金山云努力的方向。


    WebRTCon 2018 8折報名


    WebRTCon希望與行業專家一同分享、探討當下技術熱點、行業最佳應用實踐。如果你擁有音視頻領域獨當一面的能力,歡迎申請成為講師,分享你的實踐和洞察,請聯系 speaker@livevideostack.com。更多詳情掃描下圖二維碼

    總結

    以上是生活随笔為你收集整理的FFmpeg从入门到精通:SEI那些事的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。