流媒体是采用流式传输方式在网络上播放的媒体格式,视频网站内容、短视频、在线直播这些视频形态,均属于流媒体的不同分支。流媒体大致包含三个层级:码流、封装和协议。从音视频编码器输出的码流,经过某种封装格式后,经过特定的协议传输、保存,构成了流媒体世界的基础功能。

在直播应用的开发过程中,如果把主播端消息事件传递到观众端,一般会以Instant Messaging(即时通讯)的方式传递过去,但因为消息分发通道和直播通道是分开的,因此消息与直播音视频数据的同步性就会出现很多问题。那么有没有在音视频内部传递消息的方法呢?答案是SEI

1. SEI的介绍

补充增强信息(Supplemental Enhancement Information)是码流范畴里面的概念,提供了向视频码流中加入信息的办法,是H.264/H.265 视频压缩标准的特性之一。SEI 有基本的特征:

  1. 并不是解码过程的必须项;
  2. 有可能对解码过程(容错、纠错)有帮助;
  3. 集成在视频码流中;

这意味着视频编码器在输出视频码流的时候,可以不提供SEI信息 。同时我们也要清楚:视频传输过程、解封装、解码环节,都可能因为某种原因丢弃SEI

在视频内容的生成端、传输过程中,都可以插入SEI 信息。插入的信息,和其他视频内容一起经过传输链路到达了消费端。那么在SEI 中可以添加哪些信息呢?这里举几个例子,用户场景可以任意扩展:

  1. 传递编码器参数;
  2. 传递视频版权信息;
  3. 传递摄像头参数;
  4. 传递内容生成过程中的剪辑事件(引发场景切换);

1.1 NAL unit类型

网络抽象层(Network Abstract Layer)简称为NAL。在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(Video Coding Layer - VCL)和网络抽象层面(Network Abstraction Layer - NAL)。VCL负责表示有效视频数据的内容,NAL 负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。NAL unitNAL的基本语法结构,它包含一个字节的头信息(NAL header)和一系列来自VCL的原始数据字节流(RBSP)。

1.1.1 H.264/AVC 中的情况

NAL unit type储存在NAL header中,在H.264/AVC标准中,可用的NAL unit type一共有17种,其中值为6时表征SEI 内容。比较常见的类型如下表所示:

NAL unit type NAL unit content
1 非IDR图像,且不采用数据划分的片段
5 IDR图像
6 补充增强信息(SEI)
7 序列参数集(SPS)
8 图像参数集(PPS)
11 流结束符

《ISO/IEC 14496-10:2014》是MPEG专家组为AVC编解码器制定的标准,H.264/AVCNAL unit类型完整定义都在该标准的7-1表中,标准一共预留了32种类型,所以在NAL header里面,用5 bits表征NAL unit type

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

  1. 第0位是禁止位0,值为1时表示语法出错;
  2. 第1~2位为参考级别(NRI,NAL ref idc);
  3. 第3~7为是NAL unit type;

nal_unit_type in H.264/AVC

NRI取值为 "00" (二进制)时,表征NAL unit不参与重建参考图像,这时的NAL unit是可以丢弃的。大于 "00"(二进制)时,NAL unit 不能被丢弃。

1.1.H.265/HEVC 中的情况

《ISO/IEC 23008-2:2015》是MPEG专家组为HEVC编解码器制定的标准,H.265/HEVCNAL unit类型完整定义都在该标准的7-1表中,可用的NAL unit type一共有40种之多,其中39和40都表征SEI内容。因为标准一共预留64种类型,所以在NAL header里面,用6 bits表征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

nal_unit_type in H.265/HEVC

1.2 SEI 类型

H.264/AVC视频编码标准中,并没有规定SEI payload type的范围,所以表征payload type的字节数是浮动的。
语法分析如下所示,当开始解析类型为SEINAL时,持续读取8bit,直到非0xff为止,然后把读取的数值累加,累加值即为SEI payload type

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

读取SEI payload sizepayload type逻辑类似,仍然是读取到0xff为止,这样可以支持任意长度的SEI payload添加。

sei_message(){payloadSize = 0while( next_bits(8) == 0xFF){ff_bytepayloadSize += 255}last_payload_size_bytepayloadSize += 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时,具体的语法解析流程如下:

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

其中uuid_iso_iec_11578的详细定义在《
ISO/IEC 11578:1996》Annex A中,大致规定了使用128bits(16个字节)来指定UUID。此处UUID可以表征写入SEI payload的角色ID,或者表征其他业务用途。剩下的payloadSize -16字节,即是业务层传递的具体内容了。
通过user_data_unregistered()语法解析可以看出,当使用SEI payload type为5时,注意事项如下:

  1. payload size应该大于16;
  2. uuid可能出现0x000000/0x000001/0x000002,需要插入0x03做防竞争处理;

构成RBSP时,都需要做RBSP拖尾处理。拖尾处理对所有SODB方式都一致。rbsp_trailing_bits()语法逻辑如下:

rbsp_trailing_bits( ){rbsp_stop_one_bitwhile( !byte_aligned( ) )rbsp_alignment_zero_bit
}

1.3 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

2. SEI的生成

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

  1. 对已有码流做filter,插入SEI NAL
  2. 视频编码时生成SEI
  3. 容器层写入时插入SEI

以下代码示例来自于FFmpeg origin/master 分支。

2.1 bsf

BitStream Filter(码流过滤)的缩写即为bsf,在不做码流解码的前提下,对已经编码后的比特流做特定的修改、调整。

bsf h264_metadata的调用

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工具时,可以使用比特流过滤器。基本的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;elseudu->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。

2.2 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 );
}
以上写入的代码逻辑和标准语法说明保持一致。

3. SEI的解析

3.1 解析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 typelength后,对未注册用户数据的提取,跳过了uuid的分析,只尝试提取了x264的build信息。总体上,并未利用SEI_USER_DATA_UNREGISTERED传递过来的其他相关参数信息。
从解码器逻辑看,H264SEIUnregistered结构体只有一个x264_build属性,并未返回实质有效数据。上层业务如果需要提取SEI_USER_DATA_UNREGISTERED,仍然需要自己提取。提取逻辑,请参考下一小节(ffplay)。

3.2 ffplay

ffplay是一个简单、常用的FFmpeg接口示例工具,常用于测试解码、播放效果。如果在ffplay中示例跑通SEI提取功能,可以很方便的移植到其他平台。
ffplay工具中,通过av_read_frame(ic, pkt);可以快速拿到当前读到的NAL unit,位于pkt->data中,可以从此取出NAL unit type,如果是SEI且是用户未注册数据类型(payload type值为5),则可以参考SEI语法继续读取UUID和其后传递的字符串。

4. 总结

限于篇幅,本文主要对H.264码流中涉及用户未注册数据的SEI进行了分析。总体而言,SEI只是视频标准里面很小的一部分,但在应用过程中,比如直播问答项目中SEI承载的信息,就极大提升了直播观看和答题操作的整体用户体验。所以说,从SEI的例子中,我们就会发现,视频标准里面还有很多金矿等待着大家的挖掘,这就是多媒体技术的魅力。

作者:金山视频云
链接:https://www.jianshu.com/p/4d9120dfcd69
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

SEI(Supplemental Enhancement Information)相关推荐

  1. 【GStreamer】基于NTP+SEI的视频流传输时延测量

    [GStreamer]基于NTP+SEI的视频流传输时延测量 本文以H.264视频流为例,用GStreamer实现插入和提取SEI(Supplemental Enhancement Informati ...

  2. 《超低延时直播白皮书》已发布,推动直播延时降低90%以上

    点击上方"LiveVideoStack"关注我们 直播行业的蓬勃发展,推动直播技术快速演进,超低延时直播技术也应运而生. 2月22日,腾讯云携手信通院联合发布<超低延时直播白 ...

  3. VideoCodec 入门篇 - 00 (编解码简介)

    目录 1.基本术语 (Basic Terminology) 1.1.图像 (Image) 1.2.像素 (Pixel) 1.3.颜色深度 (Color Depth) 1.4.分辨率 (Resoluti ...

  4. HEVC编码视频格式

    一.编码图像的格式 1. 亮度分量和色度分量 SPS 中说明了编码的 图像格式  其中chroma_format_idc表明了支持的色度格式: 为0 表示只有Y分量 没有UV 分量. 1:4个Y共用一 ...

  5. H265/HEVC 常用缩写词及其含义

    H265/HEVC 官方文档第4章缩写词的部分翻译 B Bi-predictive:双向预测,即B帧或B条带,B条带中的CU可以采用帧内或帧间预测编码,每个预测块采用双向预测方式进行预测,B条带编码时 ...

  6. RTP Payload Format for High Efficiency Video Coding (HEVC)

    版权申明:未经允许请勿转载.转载前请先联系作者(hello@yeshen.org) This memo describes an RTP payload format for the video co ...

  7. 视频编解码——视频编解码器工作原理

    目录 视频编码器基础认知 什么是视频编解码器 为什么需要视屏编解码器 视频编码器与数字视频容器 视频编码标准的历史 通用编解码器 1.图片分区 查看分区 2.预测 3.转换 使用全部像素形成每个系数 ...

  8. H265 HEVC 协议文档第4章缩写词及其含义翻译

    本文为博主原创文章,未经博主允许不得转载.(合作洽谈请联系QQ:1010316426) 缩写 含义 B Bi-predictive:双向预测,即B帧或B条带,B条带中的CU可以采用帧内或帧间预测编码, ...

  9. RTP PS PES ES H264协议学习

    参考:https://www.cnblogs.com/wainiwann/p/7477794.html https://blog.csdn.net/chenhande1990chenhan/artic ...

最新文章

  1. Java中AudioFileStream_AudioFileStream学习
  2. 父类中“this” 指向问题
  3. 网站优化之如何才能防止域名被恶意指向?
  4. express+mongodb+vue实现增删改查-全栈之路
  5. 关于centos docker版本过低导致 is not a valid repository/tag: invalid reference format
  6. 在javascript中==和===的区别
  7. 18岁初中毕业学Java_刚满十八 初中毕业 java自学完了 没学历 该怎么办?
  8. IBM推新编码系统 实现高清视频技术大突破
  9. 5个学习Linux命令站点推荐
  10. Seven times have I despised my soul 《我曾七次鄙视自己的灵魂》
  11. 判断字符串是否是空格
  12. 记2014“蓝桥杯全国软件大赛quot;决赛北京之行
  13. 关于LVDS的硬件总结
  14. 小游戏开发设计之塔防类游戏
  15. 女孩子取什么名字好听又独特?自己家的宝宝就要个好名字
  16. 对于Biotin-LC-NHS Estercas:72040-63-2与伯氨基 (-NH2) 反应的作用有哪些?
  17. 什么是云计算?(IaaSPaaS,SaaS区别)
  18. 关于VB.NET IIF函数
  19. 教师如何创建在线查分系统
  20. Libgdx播放Spine动画(2)-功能

热门文章

  1. 票务系统的主要售票方式
  2. 警惕感冒不仅会头痛脑热,还可能引发致盲性眼病
  3. 小程序获取sessionkey_微信小程序 获取session_key和openid的实例
  4. python删除系统指定文件
  5. c++ hashset的用法_c++ stl容器set成员函数介绍及set集合插入,遍历等用法举例
  6. 计算机应用基础 周凌,计算机基础毕业论文范文
  7. ora-600[kcbz_check_objd_typ]错误处理
  8. 计算机能直接识别的算法表示形式,几种常用的图像置乱算法:图像识别算法
  9. java相传韩信才智过人_韩信点兵(hanxin)
  10. 沐圣moolsun:做真正的民族品牌