H.264/AVC中完整的NAL unit類型定義請參考《ISO/IEC 14496-10:2014》,這是MPEG專家組為AVC編解碼器制定的標(biāo)準(zhǔn),H.264/AVC中NAL unit類型完整定義都在該標(biāo)準(zhǔn)的7-1表中,標(biāo)準(zhǔn)一共預(yù)留了32種類型,在NAL header里面,用5 bits表征NAL unit type。
\
  H.264/AVC中的NAL unit type
  如上圖所示,在8 bits的NAL header中:
  1.第0位是禁止位0,值為1時表示語法出錯
  2.第1~2位是參考級別(NRI,NAL ref idc)
  3.第3~7位是NAL unit type
  需要注意的是,當(dāng)NRI取值為”00″(二進(jìn)制)時,表征NAL unit不參與重建參考圖像,這時的NAL unit是可以丟棄的。大于”00″(二進(jìn)制)時,NAL unit不能被丟棄。
  H.265/HEVC中的情況
  《ISO/IEC 23008-2:2015》是MPEG專家組為HEVC編解碼器制定的標(biāo)準(zhǔn),H.265/HEVC中NAL unit類型完整定義都在該標(biāo)準(zhǔn)的7-1表中,可用的NAL unit type一共有40種之多,其中39和40都表征SEI內(nèi)容。因為標(biāo)準(zhǔn)一共預(yù)留64種類型,所以在NAL header里面,用6 bits表征NAL unit type。
\
  H.265/HEVC中的NAL unit type
  如上圖所示,在16 bits的NAL header中:
  1.第0位是禁止位0,值為1時表示語法出錯
  2.第1~6位是NAL unit type
  3.第7~12位是NUH layer id
  4.第13~15位是temporal_id
  SEI類型
  在H.264/AVC視頻編碼標(biāo)準(zhǔn)中,并沒有規(guī)定SEI payload type的范圍,所以表征payload type的字節(jié)數(shù)是浮動的。
  語法分析如下所示,當(dāng)開始解析類型為SEI的NAL時,持續(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 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
  }
  當(dāng)獲取了SEI payload類型和大小后,就進(jìn)入了實際的SEI內(nèi)容讀取。
  當(dāng)前《ISO/IEC 14496-10:2014》Annex D.1.1提供了最大到181的payload類型處理規(guī)范,由于類型可以指定任意大小,給SEI的添加、處理創(chuàng)造了很大的自由空間。
  其中SEI payload類型值為5時,指定的處理方法叫user_data_unregistered(),字面含義為未注冊的用戶數(shù)據(jù),常用于存儲編碼器的編碼參數(shù)信息,是比較常見的payload類型。
  讀取payload type為5時,具體的語法解析流程如下:
  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ī)定了使用128 bits(16個字節(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時,注意事項如下:
  1.payload size應(yīng)該大于16;
  2.uuid可能出現(xiàn)0x000000/0x000001/0x000002,需要插入0x03做防競爭處理;
  構(gòu)成RBSP時,都需要做RBSP拖尾處理。拖尾處理對所有SODB方式都一致。rbsp_trailing_bits()語法邏輯如下:
  rbsp_trailing_bits( ){
  rbsp_stop_one_bit
  while( !byte_aligned( ) )
  rbsp_alignment_zero_bit
  }
  SEI例子
  從video.js <https://github.com/videojs/video.js>的示例中下載oceans.mp4 <http://vjs.zencdn.net/v/oceans.mp4>并提取出H.264碼流如下:
\
  bitstream from oceans.mp4
  NAL header
  起始碼(暗紅底色)”0x00000001″分割出來的比特流即是NAL unit,起始碼緊跟的第一個字節(jié)(墨綠底色)是NAL header。上圖“NAL header”一共出現(xiàn)了四個數(shù)值:
  ·”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″后一個字節(jié)為“0x05”(淡黃底色)是SEI payload type,即表征SEI payload分析遵循user_data_unregistered()語法。
  SEI payload size
  “0x05”后一個字節(jié)為“0x2F”(淡藍(lán)底色)是SEI payload size,此時整個payload是47個字節(jié)。
  SEI payload uuid
  ”0x2F”隨后的16個字節(jié)即為uuid,此時uuid為
  dc45e9bd-e6d9-48b7-962c-d820d923eeef
  SEI payload content
  由于payload size是47個字節(jié),除去16字節(jié)的uuid,剩下31個字節(jié)的content。由于content是字符串,所以有結(jié)束符”0x00″,有效的30個字符內(nèi)容是:
  Zencoder Video Encoding System
  rbsp trailing bits
  47個payload字節(jié)后的”0x80″(灰底色)即是rbsp trailing bits,在user_data_unregistered()里面都是按字節(jié)寫入的,所以此時的NAL unit結(jié)尾寫入的字節(jié)一定是0x80。
  SEI的生成
  生成SEI的方式很多,大致可以有:
  1.對已有碼流做filter,插入SEI NAL
  2.視頻編碼時生成SEI
  3.容器層寫入時插入SEI
  以下代碼示例來自于FFmpeg origin/master 分支。
  bsf
  BitStream Filter(碼流過濾)的縮寫為bsf,它的作用是,在不做碼流解碼的前提下,對已經(jīng)編碼后的比特流做特定的修改、調(diào)整。
  bsf h264_metadata的調(diào)用
  使用ffmpeg工具時,可以使用比特流過濾器?;镜膄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。下面示例命令添加了類型為未注冊的用戶數(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)有一個SEI和28個SPS。輸出的oceans.sei.h264碼流中,共有28個SEI,其中第一個與輸入保持一致,剩下27個為新插入的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時,滿足以下條件之一即可以處理:
  ·讀取的access units是第一個au;
  ·當(dāng)前au包含sps;
  滿足插入SEI邏輯后,具體處理過程中:
  ·如果發(fā)現(xiàn)第一個NAL已經(jīng)是SEI,則該au不做插入SEI處理;
  ·如果au包含了IDR幀或者非IDR未分區(qū)的幀,則在其前面插入SEI信息。
  基于以上代碼,oceans.sei.h264碼流中新插入27個新的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,對應(yīng)的即是未注冊的用戶信息。在解析”ffmpeg”工具輸入過程中,將”+”號前面的字符串轉(zhuǎn)換成二進(jìn)制寫入uuid,”+”后內(nèi)容使用字符串寫入payload。
  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)語法說明保持一致。
  SEI解析
  FFmpeg在讀取和解碼NAL unit,都有相同的邏輯處理SEI。
  讀取或者解碼數(shù)據(jù)時,會調(diào)用下面函數(shù)進(jìn)行碼流的解碼,其中buf包含具體的二進(jìn)制流,buf_size是當(dāng)前碼流長度。函數(shù)內(nèi)部會解析碼流并實例出具體的NAL對象:
  //Locate in libavcodec/h264dec.c
  int decode_nal_units(H264Context *h, const uint8_t *buf, int buf_size)
  如果NAL對象類型是SEI 時,將調(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)部會判斷SEI payload type進(jìn)行不同的函數(shù)調(diào)用,如果是未注冊的用戶數(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 type和length后,對未注冊用戶數(shù)據(jù)的提取,跳過了uuid的分析,只嘗試提取了x264的build信息??傮w上,并未利用SEI_USER_DATA_UNREGISTERED傳遞過來的其他相關(guān)參數(shù)信息。
  從解碼器邏輯看,H264SEIUnregistered結(jié)構(gòu)體只有一個x264_build屬性,并未返回實質(zhì)有效數(shù)據(jù)。上層業(yè)務(wù)如果需要提取SEI_USER_DATA_UNREGISTERED,仍然需要自己提取。提取邏輯,請參考下一小節(jié)(ffplay)。
  ffplay
  ffplay是一個簡單、常用的FFmpeg接口示例工具,常用于測試解碼、播放效果。如果在ffplay中示例跑通SEI提取功能,可以很方便的移植到其他平臺。
  在ffplay中通過函數(shù)av_read_frame(ic, pkt)返回后,讀取pkt->data可以快速拿到當(dāng)前讀到的NAL unit。從data數(shù)據(jù)中取出NAL unit type,如果是SEI且是用戶未注冊數(shù)據(jù)類型(payload type值為5),則可以參考SEI語法繼續(xù)讀取UUID和其后傳遞的字符串。
  總結(jié)
  本文主要對H.264碼流中涉及用戶未注冊數(shù)據(jù)的SEI進(jìn)行了分析??傮w而言,SEI只是視頻標(biāo)準(zhǔn)里面很小的一部分,但在應(yīng)用過程中,比如直播問答項目中SEI承載的信息,就極大提升了直播觀看和答題操作的整體用戶體驗。所以說,從SEI的例子中,我們就會發(fā)現(xiàn),視頻標(biāo)準(zhǔn)里面還有很多金礦等待著大家的挖掘,這就是多媒體技術(shù)的魅力,也是金山云努力的方向。
本文作者:阿曾
分享到

songjy

相關(guān)推薦