webrtc源码阅读之h264 RTP打包

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React + TypeScript 实践

主要内容包括准备知识、如何引入 React、函数式组件的声明方式、Hooks、useRef<T>、useEffect、useMemo<T> / useCallback<T>、自定义 Hooks、默认属性 defaultProps、Types or Interfaces、获取未导出的 Type、Props、常用 Props ts 类型、常用 React 属性类…

macbook安装chatglm2-6b

1、前言 chatglm安装环境还是比较简单的&#xff0c;比起Stable diffusion安装轻松不少。   安装分两部分&#xff0c;一是github的源码&#xff0c;二是Hugging Face上的模型代码&#xff1b;安装过程跟着官方的readme文档就能顺利安装。以下安装内容&#xff0c;绝大部分是…

【C++进阶之路】模拟实现string类

前言 本文所属专栏——【C进阶之路】 上一篇,我们讲解了string类接口的基本使用&#xff0c;今天我们就实战从底层实现自己的string类&#xff0c;当然实现所有的接口难度很大&#xff0c;我们今天主要实现的常用的接口~ 一、String类 ①要点说明 1.为了不与库里面的string冲…

2.SpringBoot运维实用篇

SpringBoot运维实用篇 ​ ​ 下面就从运维实用篇开始讲&#xff0c;在运维实用篇中&#xff0c;我给学习者的定位是玩转配置&#xff0c;为开发实用篇中做各种技术的整合做好准备工作。 主要分为以下内容&#xff1a; SpringBoot程序的打包与运行配置高级多环境开发日志 ​…

Qt day3

完善文本编辑器 mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QWidget>namespace Ui { class MainWindow; }class MainWindow : public QWidget {Q_OBJECTpublic:explicit MainWindow(QWidget *parent nullptr);~MainWindow(); public slots:void…

1.计算机是如何工作的(下)

文章目录 4.编程语言&#xff08;Program Language&#xff09;4.1程序&#xff08;Program&#xff09;4.2早期编程4.3编程语言发展 5.操作系统&#xff08;Operating System&#xff09;5.1操作系统的定位5.2什么是进程/任务&#xff08;Process/Task&#xff09;5.3进程控制…

es8.8 集群安装笔记

es8.8 集群安装笔记 配置集群第一步 修改配置文件 本次安装使用centos8 3节点安装&#xff1a; 192.168.182.142 192.168.182.143 192.168.182.144 官网 可以查看详细的安装&#xff0c;安装步骤比较简单 https://www.elastic.co/guide/en/elasticsearch/reference/8.8/rpm.htm…

Cannot find tomcat-9.0.0.M21/bin/setclasspath.sh

问题描述&#xff1a;将linux上的tomcat直接拷贝到以一个路径下&#xff0c;执行sh startup.sh 报错 解决&#xff1a;修改全局变量配置文件 1、vim /etc/profile &#xff08;主要修改如下图所标记的值 &#xff09; 2、source /etc/profile &#xff08;设置环境变量立即…

DynaSLAM 2018论文翻译

Dynaslam:动态场景下的跟踪、建图和图像修复 摘要-场景刚性假设是SLAM算法的典型特征。这种强假设限制了大多数视觉SLAM系统在人口稠密的现实环境中的使用&#xff0c;而这些环境是服务机器人或自动驾驶汽车等几个相关应用的目标。 在本文中&#xff0c;我们提出了一个基于ORB…

OpenCV 入门教程:膨胀和腐蚀操作

OpenCV 入门教程&#xff1a;膨胀和腐蚀操作 导语一、膨胀操作二、腐蚀操作三、示例应用3.1 图像增强3.2 边缘检测 总结 导语 膨胀和腐蚀是图像处理中常用的形态学操作&#xff0c;用于改变图像的形状和结构。在 OpenCV 中&#xff0c;膨胀和腐蚀是基于结构元素的像素操作&…

chatgpt生成pygame opengl实现旋转用图片填充的3d三角形

import pygame from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import *def draw_triangle():vertices ((0, 2, 0), # 顶点1(-2, -2, 0), # 顶点2(2, -2, 0) # 顶点3)tex_coords ((1, 2), # 顶点1的纹理坐标(1, 1), # 顶点2的纹理坐标(2, …

R语言绘图丨论文中最常用箱线图绘制教程,自动进行显著性检验和误差线标注

多组比较式箱线图 在科研论文绘图中&#xff0c;对于多组数据进行比较一般采用箱线图的方法&#xff0c;今天分享一下这个经典数据可视化方法&#xff0c;从零开始绘制一张带误差棒并自动计算显著性比较结果的箱线图。 前言&#xff1a;箱线图有什么优势&#xff1f; 数据分布…

mac电脑上,webm格式怎么转换成mp4?

mac电脑上&#xff0c;webm格式怎么转换成mp4&#xff1f;webm格式的视频也是最近几年也越来越多的&#xff0c;小编最近就不止一次的下载到过webm格式的视频&#xff0c;很多小伙伴肯定对它还并不是很了解&#xff0c;webm是由谷歌公司所提出以及开发出来的视频文件格式&#…

python实现语音识别(讯飞开放平台)

文章目录 讯飞平台使用python实现讯飞接口的语音识别第一步&#xff1a;导入需要的依赖库第二步&#xff1a;声明全局变量第三步&#xff1a;初始化讯飞接口对象第四步&#xff1a;收到websocket建立连接后的处理函数第五步&#xff1a;收到websocket消息的处理函数第六步&…

神经网络学习小记录74——Pytorch 设置随机种子Seed来保证训练结果唯一

神经网络学习小记录74——Pytorch 设置随机种子Seed来保证训练结果唯一 学习前言为什么每次训练结果不同什么是随机种子训练中设置随机种子 学习前言 好多同学每次训练结果不同&#xff0c;最大的指标可能会差到3-4%这样&#xff0c;这是因为随机种子没有设定导致的&#xff0…

删除链表的倒数第 N 个结点——力扣19

题目描述 法一&#xff09;计算链表长度 class Solution { public:int getLength(ListNode* head){int len0;while(head){len;head head->next;}return len;}ListNode* removeNthFromEnd(ListNode* head, int n) {int len getLength(head);ListNode* dummy new ListNode …

【Hello mysql】 mysql的约束

Mysql专栏&#xff1a;Mysql 本篇博客简介&#xff1a;介绍mysql的约束 mysql的约束 表的约束空属性默认值列描述zerofill主键自增长唯一键外键总结 表的约束 为什么要有约束&#xff1f; 我们在收集一些数据的时候会要求该数据必须存在 比如说像是国家在登记公民信息的时候身…

【Linux | Shell】Linux 安全系统 —— 用户、组、文件权限 - 阅读笔记

目录 一、Linux 的安全性1.1 /etc/passwd 文件1.2 /etc/shadow 文件1.3 添加新用户 —— useradd1.4 删除用户 —— userdel1.5 修改用户 —— usermod、passwd、chpasswd 二、使用 Linux 组2.1 /etc/group 文件2.2 创建新组 —— groupadd2.3 修改组 —— groupmod 三、理解文…

Swagger-Bootstrap-UI

Swagger-Bootstrap-UI 是一个为 Swagger 提供美观、易用的界面展示和增强功能的开源项目。它通过自定义样式和交互&#xff0c;提供了更好的文档展示和交互体验&#xff0c;包括美化的界面、接口测试工具、在线调试、文档导出等功能。 更高阶的有Knife4j,Knife4j是一个集Swagg…

免费 Selenium各大浏览器驱动【谷歌chrme、火狐Firefox、IE浏览器】

aardio群 625494397 废话不多说 直接开整&#xff01; 竟然还有脸收费 服了 下载对应版本的浏览器驱动 目标网址 应用场景 Selenium库涉及到 安装selenium库 pip install selenium-i https://mirrors.aliyun.com/pypi/simple/下载对应浏览器驱动 https://registry.npmmirror.c…