webrtc之rtp协议(二): 封装H264

作者:音视频小话
原文: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规范。

webrtc之rtp协议(二): 封装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单元报文的结构格式如下图所示。

webrtc之rtp协议(二): 封装H264

信息提示: 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负载格式如下所示:

webrtc之rtp协议(二): 封装H264

组包方式:

  • 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报文。

webrtc之rtp协议(二): 封装H264

STAP报文头中字段NAL unit size是16bits的无符号大小,定位NAL单元的大小,但是并不包含本16bits,只包含NAL单元本身。STAP在RTP负载内是基于字节对齐的,但是它不比32bit字对齐。

webrtc之rtp协议(二): 封装H264

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的具体格式如下:

webrtc之rtp协议(二): 封装H264

上面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 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论