webrtc源码阅读之h264 RTP打包

news2024/11/27 8:25:13

本文来分析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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/720261.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于Java已死,看看国外开发者怎么说的

博主在浏览 medium 社区时&#xff0c;发现了一篇点赞量 1.5k 的文章&#xff0c;名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来就是《Java 已死 — 开发人员对 Java 在现代编程语言中的5个误解》。这篇文章可以…

什么是倒排索引

什么是倒排索引 搜索的核心需求是全文检索&#xff0c;全文检索简单来说就是要在大量文档中找到包含某个单词出现的位置&#xff0c;在传统关系型数据库中&#xff0c;数据检索只能通过 like 来实现&#xff0c;例如需要在酒店数据中查询名称包含公寓的酒店&#xff0c;需要通…

k8s如何对外公布一个应用程序

一&#xff1a;Kubernetes Service&#xff08;服务&#xff09;概述 事实上&#xff0c;Pod&#xff08;容器组&#xff09;有自己的 生命周期 (opens new window)。当 worker node&#xff08;节点&#xff09;故障时&#xff0c;节点上运行的 Pod&#xff08;容器组&#x…

全网最细,Fiddler抓包实战教程-辅助接口测试(二)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Fiddler的功能面板…

超低色散非球面ED镜片——减少空三分层的“秘密武器”

引言 倾斜相机作为精密光学电子仪器&#xff0c;光学部件相当于它的眼睛&#xff0c;直接关系到原始航片采集质量的高低&#xff0c;其重要性不言而喻。优质的原始航片能够降低空三分层概率&#xff0c;增加空三解算的鲁棒性&#xff0c;从而提高内业效率&#xff0c;节约项目…

Vue3中使用vee-validate进行表单验证

官网&#xff1a;Checkbox and Radio InputsValidating checkboxes and Radio inputshttps://vee-validate.logaretm.com/v4/examples/checkboxes-and-radio/ 安装vee-validate npm i -S vee-validate 示例1&#xff1a; <template><Formref"ref_form"c…

旅游卡景区小程序购票核销分销app系统

旅游业的蓬勃发展&#xff0c;景区旅游卡也逐渐成为游客出行的常见选择。为了提升景区旅游卡的购买和使用体验&#xff0c;许多景区开始采用小程序来提供购票和核销服务。同时&#xff0c;为了更好地推广景区旅游卡&#xff0c;许多景区也开发了分销App系统&#xff0c;以提供更…

linux代码提交记录 linus亲手改代码

kernel/git/torvalds/linux.git - Linux kernel source tree

【Megatron-DeepSpeed】张量并行工具代码mpu详解(一):并行环境初始化

相关博客 【Megatron-DeepSpeed】张量并行工具代码mpu详解(一)&#xff1a;并行环境初始化 【深度学习】【分布式训练】DeepSpeed&#xff1a;AllReduce与ZeRO-DP 【深度学习】混合精度训练与显存分析 【深度学习】【分布式训练】Collective通信操作及Pytorch示例 【自然语言处…

DOM“文档对象模型”

目录 DOM 1.节点层级 1.2.节点 常用节点 文档节点&#xff08;document&#xff09; 元素节点&#xff08;Element&#xff09; 属性节点&#xff08;Attribute) 文本节点&#xff08;Text&#xff09; 其他节点 DocumentType Comment DocumentFragment 1.3.节点树…

力扣 501. 二叉搜索树中的众数

题目来源&#xff1a;https://leetcode.cn/problems/find-mode-in-binary-search-tree/description/ C题解1&#xff1a;使用中序遍历将二叉搜索树有小到大放进数组中&#xff0c;在寻找最大数组。 class Solution { public:void traversal(TreeNode* node, vector<int>…

操作系统7:进程死锁

目录 1、死锁概述 &#xff08;1&#xff09;计算机中的资源分类 1.1 - 可重用性资源和消耗性资源 1.2 - 可抢占性资源和不可抢占性资源 &#xff08;2&#xff09;计算机系统中的死锁 &#xff08;3&#xff09;死锁的定义、必要条件和处理方法 3.1 - 死锁的定义 3.2 …

运输层:TCP拥塞控制

1.运输层&#xff1a;TCP拥塞控制 笔记来源&#xff1a; 湖科大教书匠&#xff1a;TCP的拥塞控制 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 网络资源&#xff1a;在计算机网络中的链路容量&#xff08;即带宽&#xff09;、交换结点中的缓存和…

windows 10 安装Python 3.10版

一、查找官网 1.python官网需要VPN(建立一个临时的、安全的连接&#xff0c;是一条穿过混乱的公用网络的安全、稳定的隧道。)&#xff0c;打开VPN进行代理之后&#xff0c;打开网址&#xff1a;https://www.python.org/ 二、查找所需下载的系统及版本 1.本机是winodws操作系统&…

计算机网络_ 1.3 网络核心 (数据交换_报文、分组交换)

计算机网络_数据交换_报文、分组交换 报文交换&#xff08;message switching&#xff09;分组交换&#xff08;package switching&#xff09;存储-转发&#xff08;store-and-forward&#xff09;传输延迟分组交换的报文交付时间 分组交换 vs 电路交换 报文交换&#xff08;m…

zabbix监控部署(都是导图!)

目录 一&#xff1a;监控软件的作用 二&#xff1a;zabbix 简介 1、zabbix 概述 2、zabbix 监控原理 3、 Zabbix 6.0 新特性 &#xff08;1&#xff09;Zabbix server高可用防止硬件故障或计划维护期的停机 &#xff08;2&#xff09;Zabbix 6.0 LTS新增Kubernetes监控功…

C语言入门篇(二)

前言   续接上一篇文章&#xff0c;此篇继续对&#xff23;语言的基础知识点进行更新。 C语言入门篇&#xff08;一&#xff09; 初始C语言 7. 选择语句8. 循环语句9. 函数9.1 函数的定义9.2 函数的调用 10. 数组10.1 数组定义10.2 数组的下标10.3 数组的使用 11. 操作符12.…

JAVA学习(九)

1.java锁 1.1 AtomicInteger 首先说明&#xff0c;此处 AtomicInteger&#xff0c;一个提供原子操作的 Integer 的类&#xff0c;常见的还有 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等&#xff0c;他们的实现原理相同&#xff0c; 区别在与运算对象类型…

搭建新版security-oauth2协议,流程代码详解,源码分析

前言&#xff1a;最近在学习搭建oauth2协议的开放平台&#xff0c;把搭建框架时的思路以及遇到的问题记录下来。 文章会持续更新&#xff0c;前期可能会比较零碎&#xff0c;最后会整合一起&#xff0c;写一篇从部署到使用、踩坑、依赖版本解决等完整文章。 使用的是Spring S…

【深度学习】深度强化学习初学者指南

一、说明 GAN&#xff08;Generative Adversarial Networks&#xff09;是一种深度学习模型&#xff0c;它由两个神经网络组成&#xff1a;一个生成网络和一个判别网络。生成网络学习如何生成类似于给定数据集的新数据&#xff0c;而判别网络则学习如何区分生成网络生成的数据和…