本文来分析webrtc打包h264 rtp包的代码,版本m98

一、RTP协议

1.1 RTP协议概述

  • 实时传输协议(RTP)是一个网络协议,它允许在网络上进行实时的音频和视频数据传输。RTP协议主要用于解决多媒体数据的实时传输问题,特别是对延迟和数据丢失敏感的应用。
  • RTP协议包括两个紧密相关的部分:RTP数据协议和RTP控制协议(RTCP)。RTP数据协议负责数据的传输,RTCP负责监控服务质量并提供同步和标识信息。
  • RTP协议并不保证数据的可靠传输,因为在实时应用中,比起保证数据的完整性,降低延迟和抖动更为重要。因此,RTP通常运行在不可靠的传输协议(如UDP)之上。

1.2 RTP协议的主要特点

  • 实时性:RTP协议能够处理实时数据的传输,包括对延迟敏感的通信,如视频会议和语音通话。
  • 多播传输:RTP支持多播,可以将数据同时发送给多个接收者。
  • 提供序列号:RTP为每个数据包提供序列号,以便接收者可以根据序列号重新组合数据。
  • 提供时间戳:RTP为每个数据包提供时间戳,以便接收者可以同步播放数据。
  • 提供有效载荷标识:RTP可以识别不同类型的有效载荷(如音频、视频等)。

1.3 RTP协议头结构

RTP header
其中:

  • V(版本号):这是一个2位的字段,用于标识RTP的版本。当前的版本是2。
  • P(填充):如果该位被设置,那么RTP头部的末尾将包含一些填充字节,这些字节不包含在包的长度中。
  • X(扩展):如果该位被设置,那么RTP头部将包含一个扩展头部。头部扩展可以用于存储信息,比如视频旋转角度。
  • CC(CSRC计数):这是一个4位的字段,表示CSRC标识符的数量。
  • M(标记):这是一个1位的字段,由具体的有效载荷格式定义其含义。例如,在音频应用中,如果M位被设置,那么这个包通常包含了一个时间段的开始。
  • PT(有效载荷类型):这是一个7位的字段,用于标识RTP包中的有效载荷数据的类型。
  • 序列号:这是一个16位的字段,用于标识发送者发送的RTP包的序列号。
  • 时间戳:这是一个32位的字段,用于标识数据的采样实例。
  • SSRC(同步源):这是一个32位的字段,用于标识RTP数据包的源。
  • CSRC(贡献源):这是一个可选的字段,用于标识对于复合包或混合音频数据的贡献源,主要用于MCU 混流。
  • Extension header(扩展头): 如果X被设置,RTP头的尾部会包含扩展头信息。

二、RTP打包H.264

2.1 H.264 NALU

H.264将视频数据划分为一系列的网络抽象层单元(NALU)。每个NALU都包含了一部分视频数据,并且具有自己的头部结构。每个NALU由一个NALU Header和RBSP组成。下图为NALU Header。
NAL unit header

  • F(禁止位):占用1位,用于指示NALU是否被禁止。
  • NRI(重要性指示):占用2位,用于示指NAL的U重要性。较高的重要性值表示NALU包含了重要的视频数据。
  • Type(类型):占用5位,用于指示NALU的类型。不同的类型对应不同的视频数据,如关键帧非、关键帧、SPS(序列参数集)等。常见的h.264naul 类型有:
    0:未使用
    1:非关键帧(P帧)
    2:关键帧(I帧)
    3:SPS(序列参数集)
    4:PPS(图像参数集)
    5:分片层(Slice Layer)
    6:扩展NALU
    7:扩展NALU
    8:扩展NALU
    9-23:保留
    24-31:未使用

2.2 RTP载荷结构

RTP载荷分为三种不同的载荷结构:

  • 单一NALU单元结构(Single NAL Unit Packet):包含一个单一的NALU,此时Type与H.264NALU保持一致。
    RTP payload format for single NAL unit packet
    在单一NALU打包的情况下,整个H.264 NALU被打包在一个RTP包中。这种方式适用于NALU的大小不超过RTP包的最大大小限制的情况。

  • 组合封包结构(Aggregation Packet):包含多个NALU。
    RTP payload format for aggregation packets
    此时Type值应该按下表设置
    Type field for STAPs and MTAPs
    webrtc中组包只支持STAP-A(Single-Time Aggregation Packet)结构。顾名思义,STAP-A是指组合成RTP包的所有NALU共享相同的时间戳。
    下图为两个NALU组合成一个RTP包的完整RTP包结构示例:
    在这里插入图片描述

  • 分片结构(Fragmentation Unit):当一个H.264 NALU比较大时,为了网络传输,可以将一个NALU拆分到多个RTP中进行传输。webrtc支持FU-A的分片结构,如果所示:
    在这里插入图片描述
    FU Indicator结构与H.264 NALU头结构相同,如果所示。其中F、NRI与H.264 NALU一致,Type的值为28\29,分别对应FU-A和FU-B。
    FU indicator
    FU header结构如图所示:
    FU header
    其中:
    S代表Start bit,如果设置为1,代表该RTP包为FU的第一个包;
    E代表End bit,如果设置为1,代表该RTP包为FU的第最后一个包;
    R代表Reserverd bit,保留位,必须设置为0;
    Type与H.264 NALU头保持一致。

2.3 打包模式

RTP规定的打包模式有三种,分别为单一NALU模式(Single NAL unit mode)、非交替模式(Non-interleaved mode)和交替模式(Interleaved mode)。webrtc支持Single NAL unit mode和Non-interleaved mode。三种打包模式与RTP载荷结构的对应关系如图所示:
Summary of allowed NAL unit types for each packetization mode

三、webrtc 打包h264流程

webrtc源码阅读之视频采集、编码、发送中,我们分析到了RTPSenderVideo::SendVideo,在这个函数中,会调用RtpPacketizer::Create对视频数据进行RTP打包,并通过RtpPacketizerH264::NextPacket将数据转换为真正的RTP包。

  std::unique_ptr<RtpPacketizer> packetizer =
      RtpPacketizer::Create(codec_type, payload, limits, video_header);
  .......
    if (!packetizer->NextPacket(packet.get()))
      return false;

我们以视频编码格式为H.264进行分析。

RtpPacketizerH264::RtpPacketizerH264(rtc::ArrayView<const uint8_t> payload,
                                     PayloadSizeLimits limits,
                                     H264PacketizationMode packetization_mode)
    : limits_(limits), num_packets_left_(0) {
  // Guard against uninitialized memory in packetization_mode.
  RTC_CHECK(packetization_mode == H264PacketizationMode::NonInterleaved ||
            packetization_mode == H264PacketizationMode::SingleNalUnit);

//对H264打包时,需要去除H264码流中的StartCode,并以NALU为单位进行打包
//根据H264码流格式,通过StartCode,区分不同NALU,每个input_fragments_元素为一个H264 NALU
  for (const auto& nalu :
       H264::FindNaluIndices(payload.data(), payload.size())) {
    input_fragments_.push_back(
        payload.subview(nalu.payload_start_offset, nalu.payload_size));
  }

  if (!GeneratePackets(packetization_mode)) { //打包为RTP包
    // If failed to generate all the packets, discard already generated
    // packets in case the caller would ignore return value and still try to
    // call NextPacket().
    num_packets_left_ = 0;
    while (!packets_.empty()) {
      packets_.pop();
    }
  }
}

3.1 解析NALU

std::vector<NaluIndex> FindNaluIndices(const uint8_t* buffer,
                                       size_t buffer_size) {
  //H264的StratCode有两种:0x00 0x00 0x01或 0x00 0x00 0x00 0x01,跟据StartCode来区分不同NALU
  std::vector<NaluIndex> sequences;
  if (buffer_size < kNaluShortStartSequenceSize)
    return sequences;

  static_assert(kNaluShortStartSequenceSize >= 2,
                "kNaluShortStartSequenceSize must be larger or equals to 2");
  const size_t end = buffer_size - kNaluShortStartSequenceSize;
  for (size_t i = 0; i < end;) {
    if (buffer[i + 2] > 1) {
      i += 3;
    } else if (buffer[i + 2] == 1) {
      if (buffer[i + 1] == 0 && buffer[i] == 0) {
        // We found a start sequence, now check if it was a 3 of 4 byte one.
        NaluIndex index = {i, i + 3, 0};
        if (index.start_offset > 0 && buffer[index.start_offset - 1] == 0)
          --index.start_offset;

        // Update length of previous entry.
        auto it = sequences.rbegin();
        if (it != sequences.rend())
          it->payload_size = index.start_offset - it->payload_start_offset;

        sequences.push_back(index);
      }

      i += 3;
    } else {
      ++i;
    }
  }

  // Update length of last entry, if any.
  auto it = sequences.rbegin();
  if (it != sequences.rend())
    it->payload_size = buffer_size - it->payload_start_offset;

  return sequences;
}

H.264原始码流的每个NALU前都会有一个StartCode(起始码)。起始码有两种格式0x00 0x00 0x01或 0x00 0x00 0x00 0x01,按这个准则,找到数据中的起始码,并以起始码的位置分割,就找到了NALU的分界。

3.2 打包单一NALU

bool RtpPacketizerH264::PacketizeSingleNalu(size_t fragment_index) {
  // Add a single NALU to the queue, no aggregation.
  size_t payload_size_left = limits_.max_payload_len;
  if (input_fragments_.size() == 1)
    payload_size_left -= limits_.single_packet_reduction_len;
  else if (fragment_index == 0)
    payload_size_left -= limits_.first_packet_reduction_len;
  else if (fragment_index + 1 == input_fragments_.size())
    payload_size_left -= limits_.last_packet_reduction_len;
  rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];
  //比较可承载负载大小与NALU大小,若NALU太大则SingleNalu模式不能打包整个NALU
  if (payload_size_left < fragment.size()) {
    RTC_LOG(LS_ERROR) << "Failed to fit a fragment to packet in SingleNalu "
                         "packetization mode. Payload size left "
                      << payload_size_left << ", fragment length "
                      << fragment.size() << ", packet capacity "
                      << limits_.max_payload_len;
    return false;
  }
  RTC_CHECK_GT(fragment.size(), 0u);
  packets_.push(PacketUnit(fragment, true /* first */, true /* last */,
                           false /* aggregated */, fragment[0]));
  ++num_packets_left_;
  return true;
}

只有在强制选择SingleNalUnit模式时才会选择这种打包方式,webrtc一般选择的是Non-interleaved mode。

3.3 打包FU-A

bool RtpPacketizerH264::PacketizeFuA(size_t fragment_index) {
  // Fragment payload into packets (FU-A).
  rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];

  PayloadSizeLimits limits = limits_;
  // Leave room for the FU-A header.
  limits.max_payload_len -= kFuAHeaderSize;
  // Update single/first/last packet reductions unless it is single/first/last
  // fragment.
  if (input_fragments_.size() != 1) {
    // if this fragment is put into a single packet, it might still be the
    // first or the last packet in the whole sequence of packets.
    if (fragment_index == input_fragments_.size() - 1) { //最后一包
      limits.single_packet_reduction_len = limits_.last_packet_reduction_len;
    } else if (fragment_index == 0) { //第一包
      limits.single_packet_reduction_len = limits_.first_packet_reduction_len;
    } else {
      limits.single_packet_reduction_len = 0;
    }
  }
  if (fragment_index != 0) //非第一包,设置第一包标志为0
    limits.first_packet_reduction_len = 0;
  if (fragment_index != input_fragments_.size() - 1) //非最后一包,设置最后一包标志位0
    limits.last_packet_reduction_len = 0;

  // Strip out the original header.
  size_t payload_left = fragment.size() - kNalHeaderSize;
  int offset = kNalHeaderSize;

  //按FU-A分包的大小,计算每个FU的载荷大小
  std::vector<int> payload_sizes = SplitAboutEqually(payload_left, limits);
  if (payload_sizes.empty())
    return false;

  for (size_t i = 0; i < payload_sizes.size(); ++i) { //根据计算出来的FU包的载荷大小进行分包
    int packet_length = payload_sizes[i];
    RTC_CHECK_GT(packet_length, 0);
    packets_.push(PacketUnit(fragment.subview(offset, packet_length),
                             /*first_fragment=*/i == 0,
                             /*last_fragment=*/i == payload_sizes.size() - 1,
                             false, fragment[0]));
    offset += packet_length;
    payload_left -= packet_length;
  }
  num_packets_left_ += payload_sizes.size();
  RTC_CHECK_EQ(0, payload_left);
  return true;
}

这是webrtc中h264最常见的RTP打包方式。根据FU-A的打包规则进行打包。

3.4 打包STAP-A

size_t RtpPacketizerH264::PacketizeStapA(size_t fragment_index) {
  // Aggregate fragments into one packet (STAP-A).
  size_t payload_size_left = limits_.max_payload_len;
  if (input_fragments_.size() == 1)
    payload_size_left -= limits_.single_packet_reduction_len;
  else if (fragment_index == 0)
    payload_size_left -= limits_.first_packet_reduction_len;
  int aggregated_fragments = 0;
  size_t fragment_headers_length = 0;
  rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];
  RTC_CHECK_GE(payload_size_left, fragment.size());
  ++num_packets_left_;

  //计算出stap-a中每个nalu所需要的空间
  auto payload_size_needed = [&] {
    size_t fragment_size = fragment.size() + fragment_headers_length;
    if (input_fragments_.size() == 1) {
      // Single fragment, single packet, payload_size_left already adjusted
      // with limits_.single_packet_reduction_len.
      return fragment_size;
    }
    if (fragment_index == input_fragments_.size() - 1) {
      // Last fragment, so STAP-A might be the last packet.
      return fragment_size + limits_.last_packet_reduction_len;
    }
    return fragment_size;
  };

  while (payload_size_left >= payload_size_needed()) {
    RTC_CHECK_GT(fragment.size(), 0);
    packets_.push(PacketUnit(fragment, aggregated_fragments == 0, false, true,
                             fragment[0]));
    payload_size_left -= fragment.size();
    payload_size_left -= fragment_headers_length;

    fragment_headers_length = kLengthFieldSize;
    // If we are going to try to aggregate more fragments into this packet
    // we need to add the STAP-A NALU header and a length field for the first
    // NALU of this packet.
    if (aggregated_fragments == 0)
      fragment_headers_length += kNalHeaderSize + kLengthFieldSize;
    ++aggregated_fragments;

    // Next fragment.
    ++fragment_index;
    if (fragment_index == input_fragments_.size())
      break;
    fragment = input_fragments_[fragment_index];
  }
  RTC_CHECK_GT(aggregated_fragments, 0);
  packets_.back().last_fragment = true;
  return fragment_index;
}

3.5 NextPacket

bool RtpPacketizerH264::NextPacket(RtpPacketToSend* rtp_packet) {
  RTC_DCHECK(rtp_packet);
  if (packets_.empty()) {
    return false;
  }

  PacketUnit packet = packets_.front();
  if (packet.first_fragment && packet.last_fragment) {   //如果一个包既是第一包又是最后一包,则说明这是Single NAL unit包
    // Single NAL unit packet.
    size_t bytes_to_send = packet.source_fragment.size();
    uint8_t* buffer = rtp_packet->AllocatePayload(bytes_to_send);
    memcpy(buffer, packet.source_fragment.data(), bytes_to_send);
    packets_.pop();
    input_fragments_.pop_front();
  } else if (packet.aggregated) {  //组合包 stap-a
    NextAggregatePacket(rtp_packet);
  } else { //分片模式 fu-a
    NextFragmentPacket(rtp_packet);
  }
  rtp_packet->SetMarker(packets_.empty());  //如果是最后一包,则置marker位
  --num_packets_left_;
  return true;
}

根据打包类型,调用不同函数。

  • 如果是Single NAL unit packet,则按Single NALU规则直接打进RTP包里。
  • 如果是组合包,则调用NextAggregatePacket进行封装,封装规则与2.2中组合封包结构(Aggregation Packet)一致。
  • 如果是组合包,则调用NextFragmentPacket进行封装,封装规则与2.2中分片结构(Fragmentation Unit)一致。
  • 如果是最后一包,会把RTP头的marker位置1,用来代表帧结束标志。

至此,webrtc将H.264打包位RTP包的过程已经分析完毕。

参考资料

RTP: A Transport Protocol for Real-Time Applications(RFC3550)
RTP Payload Format for H.264 Video(RFC6184)

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐