作者:音视频小话
原文:https://mp.weixin.qq.com/s/MjvUuRBp_blP7NOS_z-r4Q
上一篇博文介绍Rtp协议(webrtc之rtp协议)的格式:
- Rtp公共头
- Rtp扩展头
Webrtc在对媒体编码H264进行rtp封装,在RFC6184有详细解释。本文介绍:
- rtp如何封装H264
- 代码实现
1 Rtp封装H264主要的三种方式
负载格式定义了3种不同的基础负载结构。接收者可以通过负载数据的第一个字节来确定该负载的结构类型。这个字节也用用来表示NAL单元类型。NAL单元类型字段定义本报文后面的负载数据的结构类型。下面介绍3种主要的结构类型:
- Single NAL Unit Packet:该负载类型只包含一个NAL单元。NAL单元类型字段值范围为1到23。
- Aggregation Packet: 该类型包含多个NAL单元到一个RTP负载中。Single-Time Aggregation Packet Type A(STAP-A)。在webrtc中,通常一个Rtp包同时放入sps和pps。
- Fragementation unit: 改类型将一个NAL单元分片到多个RTP报文中。有两个版本: FUa, FUb, 类型只为28, 29。webrtc中,只用到FUa类型。
NAL Unit Packet Packet Type Name SectionType Type-------------------------------------------------------------
0 reserved -
1-23 NAL unit Single NAL unit packet 5.6
24 STAP-A Single-time aggregation packet 5.7.1
25 STAP-B Single-time aggregation packet 5.7.1
26 MTAP16 Multi-time aggregation packet 5.7.2
27 MTAP24 Multi-time aggregation packet 5.7.2
28 FU-A Fragmentation unit 5.8
29 FU-B Fragmentation unit 5.8
30-31 reserved
如上表,就是类型与H264 Nalu type的对应关系
+-+-+-+-+-+-+-+-+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+-+-+-+-+-+-+-+-+
上面就是NaluType的头,其中字段F和NRI的语义如下:
* F: 1 bit, forbidden_zero_bit
0代表NAL单元类型字段和其内容可能存在bit级别的错误或其他语义错误部分。1代表包含bit不存在bit级别的错误或其他语义错误部分。MANEs建议设置F位表明是否在NAL单元中有bit级别的错误。H264规范要求设置F为0。当F为1时,解码器就得知NAL单元中可能存在bit错误或其他语义错误。最简单的处理方法就是丢弃该负载,并且做错误补偿处理
* NRI: 2 bits, nal_ref_idc
该字段的意义同H264规范要求没有什么不同。也就是说,00代表该NAL单元内容并不用来为I帧来重构参考帧,这样的帧即使被丢弃也不会影响到参考帧的完整性。该字段大于00时,代表这个NAL单元的界面会影响到参考帧的完整性。
除了上面的规定外,该字段对于RTP协议来说,也由编码器定义的RTP报文优先级。MANEs用该字段来更好的保护重要的NAL数据,相比不那么重要的NAL数据。最高级别是11,其次是10,后面是01,最后是00.
H264编码器必须根据H264规范设置NRI字段,基于NAL单元类型值1到12内。基于规范,当NAL单元类型为6,9,10,11或12,对NRI字段设置为0.
NAL单元类型值为7、8(也就是常说的sps,pps), H264编码器应该设置NRI值为11。对于NAL单元类型为5(常说的IDR帧,也就是I帧),NRI只也应该设置成11.
对于NAL单元类型到NRI值的映射,接下来的例子显示了在一定环境下的有效性。其他的映射关系也应该类似这样,必须基于应用场景和H264规范。
提示: 如上所示,非参考帧的类型,NRI值为00.
H264编码器应该设置NRI值为01,针对参考帧的coded sliced和coded slice data partition NAL单元。
针对NRI值的NAL类型值为24,29,将在后面进行描述。
对于NAL单元type值13到23的对应NRI值是没有推荐值的,因为这些值是在ITU-T和ISO/IEC。对于NAL单元type值0和范围30到31,也是没有推荐值,这些值在备忘录中也是没有语义规定。
1.1 Single NAL Unit Packet (单NAL单元报文)
单NAL单元报文定义: 只包含一个NAL单元。也就是说聚合报文和分片报文都不能放在单NAL单元报文中。单个NAL单元报文构造的时候,必须和RTP sequence的顺序是一致的。单NAL单元报文的结构格式如下图所示。
信息提示: NAL单元第一个字节也是作为RTP负载的头。
1.2 Aggregation Packets (聚合报文)
聚合报文是一种负载定义的NAL单元聚合方案。这个方案主要用于适应不同网络MTU大小的情况: 有线IP网络(如Ethernet的MTU大小为1500字节),和基于IP或非IP的无线网MTU254字节或者更小。为了防止两个网络之间的转换,和提前避免不希望的组包大小,NAL单元的聚合组包方案就被设计出来了。
主要的聚合报文定义:
* Single-time aggregation packets(STAP): 聚合报文内的NAL单元时间戳都一致。定义了两种STAP报文: 非DON(STAP-A)和DON(STAP-B).
聚合报文的RTP负载格式如下所示:
组包方式:
- RTP时间戳必须设置成包内所有NAL单元最早的时间戳
- NAL单元类型值必须设置成正确的值
- 如果所有的聚合NAL单元都是需要设置F标志位为0,F标志位必须为0;否则为1
- NRI的值应该是NAL单元中最大的值
RTP头中的marker标志位应该被设置成聚合包中最后一个NAL单元对应的marker标志位。
聚合报文的负载由一个或多个聚合NAL单元组成。聚合报文会尽可能的包含多个NAL报文;然而,聚合报文总的大小还是要包含在IP报文内,并让整个IP报文小于MTU大小。
STAP必须是包内的NAL单元时间戳都一样。STAP-A报文负载不包含DON,内部包含至少一个NAL报文。
STAP报文头中字段NAL unit size是16bits的无符号大小,定位NAL单元的大小,但是并不包含本16bits,只包含NAL单元本身。STAP在RTP负载内是基于字节对齐的,但是它不比32bit字对齐。
STAP-A的例子。这个STAP包含两个STAP单元,NALU1和NALU2。
1.3 Fragmentation Units(FUs–分片单元)
这个负载方式能把NAL单元分片后放入多个RTP报文中去。分片放在RTP这样的应用层,而不是网络层(如IP层)有如下的优点:
- 能承载NAL单元大小超过64kbytes在IP网络的传输,特别是高质量视频格式(因为每幅图限制其中slice的个数,也就是限制每幅图的NAL单元总数)
- 分片机制能分片一个NAL单元,并加入FEC前向纠错报文到其中。
- 分片只能针对一个NAL单元,而不能对聚合报文。NAL单元的分片组成一串连续的NAL字节。NAL单元中的每一个字节都必须是NAL分片中的其中一部分。同一个NAL单元的分片必须按照顺序放入对应的RTP顺序报文中(在这些分片的RTP报文中,不能夹杂其他任何RTP报文)。同样的,接收到对这些分片也必须根据RTP sequence顺序来重组。
- 当NAL单元被分片并在分片单元中传输,就叫它分片NAL单元。STAPs和MTAPs都不能被分片。FUs也不能被递归分片,一个FU不能再被分片。
为FU-A的报文格式。一个FU-a由一个字节的FU indicator,一个字节的FU header和分片载荷FU payload。FU indicator的具体格式如下:
上面FU indicator的Type值28、29分别表示FU-A、FU-B。F标志位在5.3节介绍过。NRI的值根据分片NAL单元来设置。
FU header格式如下:
S: 1 bit
设置成1,表示第一个分片的开始。当这个分片负载内容不是第一个分片,设置为0
E: 1 bit
设置成1,表示当前是分片的最后一个报文,也就是说,负载的最后一个字节也就是被分片NAL单元的最后一个字节。如果该FU的负载数据不是最后一个分片,设置为0
R: 1 bit
当前设置为0,保留位,当前接收者不处理该位。
Type: 5 bits
该字段为NAL单元的负载类型,常规:5为IDR,6为SEI,7为SPS,8为PPS。
一个NAL的分片不能都放在一个FU中传输;也就是说开始和结束标志位不能同时设置成1。被分片的NAL单元有很多FU负载组成,也就是说连续的FU分片负载能重新组装NAL单元。
NAL单元类型并不放在FU负载中,但是可以通过FU indicator中的高3位,和FU header中的低5位来组成NAL单元类型。
如果一个分片丢失,那么这个NAL单元的其他分片也就该被丢弃。
如果接受者接收的到n-1个报文,少一个的话,可以组成不完整报文,并且设置forbidden_zero_bit字段为1,表示有语法错误。
2 代码实现
这里介绍如何通过Rtp报文组装H264,用开源cpp_streamer作为例子,开源地址:
https://github.com/runner365/cpp_streamer/blob/v1.1/src/net/webrtc/pack_handle_h264.cpp
https://github.com/runner365/cpp_streamer/blob/v1.1/src/net/webrtc/pack_handle_h264.hpp
cpp_streamer支持多种流媒体协议和格式。rtp组装H264主要由两个文件来完成:
- pack_handle_h264.cpp
- pack_handle_h264.hpp
2.1 入口解析
首先通过NaluType,区分三个类型的组包类型:
- SingleNalu
- StapA
- FuA
在函数InputRtpPacket导入rtp报文,并使用NaluType来区分类型。
void PackHandleH264::InputRtpPacket(std::shared_ptr<RtpPacketInfo> pkt_ptr) {
if ((nal_type >= 1) && (nal_type <= 23)) {//single nalu
//处理single nalu类型
}else if (nal_type == 28) {//rtp fua
//处理rtp fua类型,分片类型,需要缓存来进行组装
}else if (nal_type == 24) {//handle stapA
//处理stapA类型,把一个报文内的多个H264报文分离出来
}
}
区分后,分别解析三种NaluType的组包。
2.2 解析single nalu类型的Rtp包
只有一个H264帧在Rtp包内。
void PackHandleH264::InputRtpPacket(std::shared_ptr<RtpPacketInfo> pkt_ptr) {
uint8_t* payload_data = pkt_ptr->pkt->GetPayload();
uint8_t nal_type = payload_data[0] & 0x1f;
if ((nal_type >= 1) && (nal_type <= 23)) {//single nalu
//处理single nalu类型
int64_t dts = pkt_ptr->pkt->GetTimestamp();
size_t pkt_size = sizeof(NAL_START_CODE) + pkt_ptr->pkt->GetPayloadLength() + 1024;
auto h264_pkt_ptr = std::make_shared<Media_Packet>(pkt_size);
h264_pkt_ptr->buffer_ptr_->AppendData((char*)NAL_START_CODE, sizeof(NAL_START_CODE));
h264_pkt_ptr->buffer_ptr_->AppendData((char*)payload_data, pkt_ptr->pkt->GetPayloadLength());
h264_pkt_ptr->av_type_ = MEDIA_VIDEO_TYPE;
h264_pkt_ptr->codec_type_ = MEDIA_CODEC_H264;
h264_pkt_ptr->fmt_type_ = MEDIA_FORMAT_RAW;
h264_pkt_ptr->dts_ = dts;
h264_pkt_ptr->pts_ = dts;
h264_pkt_ptr->is_seq_hdr_ = false;
h264_pkt_ptr->is_key_frame_ = false;
cb_->MediaPacketOutput(h264_pkt_ptr);
}
}
取出时间戳,和nalu数据。
2.3 解析fuA nalu类型的Rtp包
fuA类型是H264较大,分成多个分片,需要进行缓存和组包。
void PackHandleH264::InputRtpPacket(std::shared_ptr<RtpPacketInfo> pkt_ptr) {
uint8_t* payload_data = pkt_ptr->pkt->GetPayload();
uint8_t nal_type = payload_data[0] & 0x1f;
if ((nal_type >= 1) && (nal_type <= 23)) {//single nalu
//处理single nalu类型
}else if (nal_type == 28) {//rtp fua
//处理rtp fua类型,分片类型,需要缓存来进行组装
bool start = false;
bool end = false;
GetStartEndBit(pkt_ptr->pkt, start, end);
if (start && !end) {
//有开始标识,无结束标识。
//表示分片开始,清空之前的缓存,开始缓存第一个报文
packets_queue_.clear();
start_flag_ = start;
} else if (start && end) {//exception happened
//不可能有开始和结束同时存在,上报异常
LogErrorf(logger_, "rtp h264 pack error: both start and end flag are enable"); ResetRtpFua();
ReportLost(pkt_ptr);
return; }
//报文缓存在报文队列中
packets_queue_.push_back(pkt_ptr);
//如果有开始和结束标识后,证明整帧凑齐,进行组装。
if (start_flag_ && end_flag_) {
auto h264_pkt_ptr = std::make_shared<Media_Packet>(50*1024);
int64_t dts = 0;
bool ok = DemuxFua(h264_pkt_ptr, dts);
if (ok) {
h264_pkt_ptr->av_type_ = MEDIA_VIDEO_TYPE;
h264_pkt_ptr->codec_type_ = MEDIA_CODEC_H264;
h264_pkt_ptr->fmt_type_ = MEDIA_FORMAT_RAW;
h264_pkt_ptr->dts_ = dts;
h264_pkt_ptr->pts_ = dts;
nal_type = ((uint8_t*)h264_pkt_ptr->buffer_ptr_->Data())[4];
nal_type = nal_type & 0x1f;
if ((nal_type == kAvcNaluTypeSPS) || (nal_type == kAvcNaluTypePPS)) {
h264_pkt_ptr->is_seq_hdr_ = true;
h264_pkt_ptr->is_key_frame_ = false;
} else if (nal_type == kAvcNaluTypeIDR) {
h264_pkt_ptr->is_seq_hdr_ = false;
h264_pkt_ptr->is_key_frame_ = true;
} else {
h264_pkt_ptr->is_seq_hdr_ = false;
h264_pkt_ptr->is_key_frame_ = false;
}
cb_->MediaPacketOutput(h264_pkt_ptr);
} else {
ReportLost(pkt_ptr);
}
start_flag_ = false;
end_flag_ = false;
return;
}
}
组包成包后,再回调上送。
2.4 解析StapA nalu类型的Rtp包
StapA类型是H264较小,多个H264帧放入同一个Rtp包。一般就是sps和pps放入一个Rtp报文。
void PackHandleH264::InputRtpPacket(std::shared_ptr<RtpPacketInfo> pkt_ptr) {
if ((nal_type >= 1) && (nal_type <= 23)) {//single nalu
//处理single nalu类型
}else if (nal_type == 28) {//rtp fua
//处理rtp fua类型,分片类型,需要缓存来进行组装
}else if (nal_type == 24) {//handle stapA
//处理stapA类型,把一个报文内的多个H264报文分离出来
bool ret = DemuxStapA(pkt_ptr);
if (!ret) {
ReportLost(pkt_ptr);
}
}
}
如上,分离出sps和pps后,回调上送。
3 总结
Webrtc在对媒体编码H264进行rtp封装,在RFC6184有详细解释。本文介绍3种Rtp组包和代码实现:
- Single模式: 单个H264帧放入Rtp包
- StapA模式: 多个H264帧放入Rtp包,一般为sps和pps放入Rtp包
- FUa模式:一个H264较大,分成多个分片后,放入多个rtp包中
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。