关于webrtc:WebRTC-中如何自定义-RTCP-信息并使用

49次阅读

共计 15162 个字符,预计需要花费 38 分钟才能阅读完成。

简介

​ 笔者目前在做一个基于 WebRTC 的 Android 端 P2P 视频流传输我的项目,现有 需要:Sender(视频发送方)在向 Receiver(视频接管方)传输肯定数量的帧之后,进行向 Receiver 传输视频数据。因为 Sender 无奈预知接管方何时能力接管完这些帧,进而无奈预知本人何时能力进行传输视频数据,因而须要 Receiver 在接管完这些帧之后向 Sender 反馈,告诉 Sender 进行发送视频数据。

​ WebRTC 应用 RTP 和 RTCP 协定进行媒体通信。简略地说,RTP(实时传输协定 / Real-time Transport Protocol)用于传输音视频数据,RTCP(RTP 控制协议 / RTP Control Protocol)用于传输通信过程中的元数据(如丢包率、关键帧申请)。前述 Receiver 向 Sender 反馈能够应用 RTCP 实现,具体能够参考申请关键帧的过程。

准备常识

RTCP

​ RFC 3550 中对于 RTCP 的定义为:RTCP 应用与传输数据 分组 (packet)雷同的 散发(distribution)机制,对会话中的所有参与者进行周期性的管制分组传输。该文件同时定义了几种 RTCP 分组类型,包含 SR、RR、SDES、BYE 和 APP。RFC 4585 扩大了 RTCP 协定,容许接管方向发送方提供更立刻的反馈,进而能够实现短时间内自适应的、基于反馈的高效的修复机制。该文件定义了一种新的 RTCP 分组类型,即反馈(FB)信息,对 FB 信息进一步分类并定义了几种 FB 信息,包含通用 NACK、PLI、SLI、RPSI 等。

​ RTCP 分组不能独自应用,必须要将多个 RTCP 分组连结造成一个 复合(compound)RTCP 分组,之后再通过低层协定(如 UDP)发送进来。复合 RTCP 分组内的各个 RTCP 分组之间不须要分隔符,这是因为对于 RTCP 分组的对齐要求以及每个分组的长度域曾经足够保障所有分组都能够被正确地宰割。上图展现了一个 RTCP 复合分组的例子,其中蕴含加密前缀、SR 分组、SDES 分组以及一个 BYE 分组。RFC 3550 规定一个 RTCP 复合分组中必须至多蕴含一个 SR/RR 分组和一个 SDES 分组。

RTCP FB 信息 是一种与 SR、RR 等并列的 RTCP 分组类型,因而 FB 信息也能够退出到单个复合 RTCP 分组当中,与其余 RTCP 分组一起发送进来。依据档次的不同,FB 信息能够被分为:

  • 传输层 FB 信息:用于通用目标的反馈信息,即与特定编解码或利用无关的信息。目前只定义了通用 NACK 信息。
  • 特定负载 FB 信息:随负载类型不同而含意不同的信息,这些信息产生并作用于编解码“层”。目前定义的特定负载 FB 信息包含 PLI(图像失落批示 / Picture Loss Indication)、SLI(切片失落批示 / Slice Loss Indication)、RPSI(参考图像抉择批示 / Reference Picture Selection Indication)。
  • 应用层 FB 信息:利用自定义的反馈信息。从 RTCP 协定的角度来看,应用层 FB 信息能够视为一类非凡的特定负载 FB 信息。

​ 一个 RTCP FB 信息的分组格局如上图所示,其中蕴含了 3 个与分组类型相干的域:FMT(反馈信息类型 / Feedback Message Type)、PT(负载类型 / Payload Type)、FCI(反馈管制信息 / Feedback Control Information):

  • FMT:指明了 FB 信息的类型,随着 PT 的不同,其解释不同。比方,当 FMT 域的值为 1 时,如果以后分组是传输层 FB 信息(PT 域的值为 205)则代表通用 NACK 信息,如果以后分组是特定负载 FB 信息(PT 域的值为 206)则代表 PLI 信息。
  • PT:指明以后分组属于 RTCP FB 信息,其值为 205 时代表 RTPFB(传输层 FB 信息),其值为 206 时代表 PSFB(特定负载 FB 信息,同时蕴含了应用层 FB 信息)。
  • FCI:不同类型的反馈所须要的额定信息不尽相同,这些额定信息寄存在 FCI 域。FCI 域的长度随反馈类型的不同而不同。

申请关键帧

​ 在视频流传输的过程中,数据分组可能产生失落,导致解码器无奈进行失常的解码。通过 PLI 信息,解码器告诉编码器失落了一张或多张图像的编码视频数据,进而帧间预测的预测链可能曾经被突破。编码器在收到 PLI 信息之后,个别会向解码器发送一张帧内编码图像(即关键帧)以复原预测链。从成果上来看,PLI 信息与在 RFC 2032 中定义的 FIR(全帧内编码帧申请 / Full Intra-frame Request)信息类似。

​ 博客 WebRTC 钻研:关键帧申请中展现了几个申请关键帧的场景,包含失落包过多、获取帧数据超时、解码出错等。上面将针对解码出错的场景,联合 WebRTC 中的相干实现展现申请关键帧的过程。其余场景在调用 VideoReceiveStream::RequestKeyFrame() 函数之后的过程雷同。

WebRTC 中的相干实现

​ 笔者所应用的 WebRTC 版本:M74。

​ 上图展现了申请关键帧过程中次要波及到的类,无关的成员变量和成员函数未显示在图中。其中,外围局部是蓝色线框内的 RTP / RTCP 模块,包含定义了模块接口的 RtpRtcp 类、模块的实现 ModuleRtpRtcpImpl 类、RTCP 发送器 RTCPSender 类、RTCP 接收器 RTCPReceiver 类;绿色线框和紫色线框内的类属于视频发送方,包含 RTP 视频发送器 RtpVideoSender 类、用于编码器解决 RTCP 反馈的 EncoderRtcpFeedback 类;红色线框内的类属于视频接管方,包含 RTP 视频流接收器 RtpVideoStreamReceiver 类、解决视频流从接管到解码再到渲染全过程的 VideoReceiveStream 类。

​ 在解码出错的场景中,VideoReceiveStream 实例会在执行 HandleEncodedFrame() 函数时发现视频解码呈现问题,并调用本人的 RequestKeyFrame() 函数以申请关键帧,一路调用至 ModuleRtpRtcpImpl 实例的 RequestKeyFrame() 函数。之后,通过 构建 (build)PLI 分组、分组连结造成 RTCP 复合分组,一个 PLI 信息得以通过 RTCPSender 实例的 SendCompoundRTCP() 函数发送到网络中,而后被视频发送方接管到。视频发送方的 RTCPReceiver 实例在 IncomingPacket() 函数中解决接管到的 RTCP 复合分组,解析并辨认出 PLI 分组,之后触发相应的回调函数,即 EncoderRtcpFeedback 实例的 OnReceivedIntraFrameRequest() 函数。一个 EncoderRtcpFeedback 实例与一个视频流编码器实例相关联,能够操纵编码器向解码器发送关键帧。至此,由解码器申请关键帧到编码器发送关键帧的过程完结。这一过程并不简单,然而却牵扯了较多的类,从中能够琢磨 WebRTC 甚或软件设计时的一些 要点

  • 接口与实现拆散。如 RTP / RTCP 模块,RtpRtcp 类定义模块的接口,ModuleRtpRtcpImpl 实现模块,其余类在应用 RTP / RTCP 模块时只需晓得该模块的接口(RtpStreamSender 类和 RtpVideoStreamReceiver 类只需领有 RtpRtcp 类型的指针),无需晓得模块如何实现。
  • 接口该当足够简略明确,实现则不用。如 KeyFrameRequestSender 类和 VideoReceiveStream 类,KeyFrameRequestSender 类作为接口只要求实现 RequestKeyFrame() 申请关键帧办法,而 VideoReceiveStream 类除了实现 KeyFrameRequestSender 接口之外,还实现了 rtc::VideoSinkInterface<VideoFrame>NackSendervideo_coding::OnCompleteFrameCallback 等诸多接口(未在上图中画出),自身须要承当治理视频接管、解码、渲染等诸多职责。
  • 利用“两头类”为零碎组件解耦。如 RTCP 接收器在辨认出 PLI 信息之后,并不间接告诉编码器发送关键帧(即编码器并非 RTCP PLI 信息的观察者),而是告诉“两头类”EncoderRtcpFeedback,再由其告诉编码器发送关键帧。乍看之下,仿佛也能够让编码器间接实现 RtcpIntraFrameObserver,而不须要由 EncoderRtcpFeedback 与编码器关联后再去实现 RtcpIntraFrameObserver 接口的函数。但实际上,编码器所要解决的 RTCP 反馈不止 PLI 一种,如果让编码器类间接实现 RTCP 观察者接口,则编码器类须要实现多个接口,且每次欲新增与编码器无关的 RTCP 分组类型时,都须要改变编码器类(包含编码器接口类和编码器实现类)的代码;另一方面,编码器在解决 RTCP 反馈信息时的行为是对立的,因而能够将这些行为形象进去,封装在 EncoderRtcpFeedback 类中。

​ 以上叙述展现了从解码器申请关键帧到编码器发送关键帧的过程,但尚未深究细节,如 PLI 信息在 WebRTC 中是如何定义的。下一节将联合 WebRTC 代码展示更多的细节,并逐渐实现笔者的需要。

实现

​ 上面将参考申请关键帧的实现,形容如何在 WebRTC 中自定义 RTCP 信息并应用,包含四个步骤:自定义 RTCP 分组类、批改 RTP / RTCP 模块、批改 RTCP 发送方相干代码、批改 RTCP 接管方相干代码。出于笔者的需要,自定义 RTCP 信息的实现具备针对性。如果读者想要定义本人的 RTCP 信息,则参考以下步骤略作批改即可。另外须要留神的是,援用的代码可能并不残缺,与主题无关的局部应用 // ... ... 省略。

自定义 RTCP 分组类

​ PLI 分组类的定义如下,其继承自 Psfb 类(Psfb 类又继承自 RtcpPacket 类),并蕴含一个名为 kFeedbackMessageType 的属性。Psfb 即准备常识中提及的特定负载 FB 信息,kFeedbackMessageType 则对应于 RTCP FB 信息分组格局的 FMT 域。该类仅蕴含 3 个成员函数,且定义与实现都较为简单。这是因为在申请关键帧时,编码器并不需要额定的信息,只须要晓得这是一个 PLI 分组即可,其 FCI 域不须要承载任何信息。

/* modules/rtp_rtcp/source/rtcp_packet/pli.h */

class Pli : public Psfb {
 public:
  static constexpr uint8_t kFeedbackMessageType = 1;

  Pli();
  Pli(const Pli& pli);
  ~Pli() override;

  bool Parse(const CommonHeader& packet);

  size_t BlockLength() const override;

  bool Create(uint8_t* packet,
              size_t* index,
              size_t max_length,
              PacketReadyCallback callback) const override;
};

​ 依据笔者的需要,当视频接管方心愿进行视频流传输时,仅需告诉视频发送方即可,不须要额定的信息。因而自定义的 RTCP 分组类(在此命名为 SVSI 类)能够与 Pli 类一样简略。须要留神的是,笔者认为 SVSI 信息特定于笔者的利用,故将其定义为应用层 FB 信息。对于 FMT 域的设置,在 SVSI 类中不须要定义 kFeedbackMessageType 成员变量,在发明和解析 SVSI 分组时间接应用其父类成员变量 Psfb::kAfbMessageType 以表明本人是应用层 FB 信息即可。

bool Svsi::Parse(const CommonHeader& packet) {RTC_DCHECK_EQ(packet.type(), kPacketType);  // 查看是否是特定负载 FB 信息
  RTC_DCHECK_EQ(packet.fmt(), Psfb::kAfbMessageType);  // 查看是否是应用层 FB 信息

  if (packet.payload_size_bytes() < kCommonFeedbackLength) {RTC_LOG(LS_WARNING) << "Packet is too small to be a valid SVSI packet";
    return false;
  }

  ParseCommonFeedback(packet.payload());  // 解析分组发送器和媒体源的 ssrc
  return true;
}
bool Svsi::Create(uint8_t* packet,
                 size_t* index,
                 size_t max_length,
                 PacketReadyCallback callback) const {while (*index + BlockLength() > max_length) {if (!OnBufferFull(packet, index, callback))
      return false;
  }

  CreateHeader(Psfb::kAfbMessageType, kPacketType, HeaderLength(), packet,
               index);  // 设置为特定负载 FB 信息、应用层 FB 信息
  CreateCommonFeedback(packet + *index);  // 设置分组发送器和媒体源的 ssrc
  *index += kCommonFeedbackLength;
  return true;
}

RTP / RTCP 模块批改

​ 自定义 RTCP 分组类之后,对于 RTP / RTCP 模块的批改次要包含:(模块)对外提供发送自定义 RTCP 分组的接口以及容许设置接管到自定义 RTCP 分组后的回调、(RTCP 发送器)提供并注册构建自定义 RTCP 分组的函数、(RTCP 接收器)提供解析自定义 RTCP 分组的能力并在解析后告诉对应的观察者。

模块的接口与实现

​ 在新增一种 RTCP 分组之后,作为一个模块接口,RtpRtcp 类该当满足以下要求:

  • 提供一个简略的函数,调用者能够通过该函数发送新增类型的 RTCP 分组;
  • 在模块创立时传入一个观察者类的实例,以便于模块在接管到新增类型的 RTCP 分组之后能够告诉该观察者进行相应的解决。

​ 因而,在 RtpRtcp 类中增加一个发送 SVSI 信息的接口,

/* modules/rtp_rtcp/include/rtp_rtcp.h */

class RtpRtcp : public Module, public RtcpFeedbackSenderInterface {
 public:
  // ... ...
  
  // Sends a request for a keyframe.
  // Returns -1 on failure else 0.
  virtual int32_t RequestKeyFrame() = 0;

  // (cf) Sends a SVSI message.
  // Returns -1 on failure else 0.
  virtual int32_t IndicateStopVideoStreaming() = 0;  // 发送 SVSI 信息的接口

  // Sends a LossNotification RTCP message.
  // Returns -1 on failure else 0.
  virtual int32_t SendLossNotification(uint16_t last_decoded_seq_num,
                                       uint16_t last_received_seq_num,
                                       bool decodability_flag) = 0;
};

​ 在 RTP / RTCP 模块的定义文件中申明 SVSI 信息的观察者类,并在用于初始化模块的配置类中退出该观察者类。

/* modules/rtp_rtcp/include/rtp_rtcp_defines.h */

class RtcpIntraFrameObserver {
 public:
  virtual ~RtcpIntraFrameObserver() {}

  virtual void OnReceivedIntraFrameRequest(uint32_t ssrc) = 0;
};

// (cf) Observer for incoming StopVideoStreamingIndication RTCP messages.
class RtcpSVSIObserver {  // 接管到 SVSI 信息的观察者
 public:
  virtual ~RtcpSVSIObserver() {}

  virtual void OnReceivedSVSI(uint32_t ssrc) = 0;
};
/* modules/rtp_rtcp/include/rtp_rtcp.h */

class RtpRtcp : public Module, public RtcpFeedbackSenderInterface {
 public:
  struct Configuration {
    // ... ...
    
    // Called when the receiver requests an intra frame.
    RtcpIntraFrameObserver* intra_frame_callback = nullptr;

    // (cf) Called when the receiver send SVSI packet.
    RtcpSVSIObserver* svsi_callback = nullptr;
    
    // ... ...
  };
  // ... ...
};

​ RTP / RTCP 模块中 RTCP 分组发送和接管的实现借助 RTCP 发送器和 RTCP 接收器。发送 RTCP 分组时,须要向 RTCP 发送器指定分组类型;接管 RTCP 分组之前(初始化 RTCP 接收器时),须要向 RTCP 接收器指定观察者。上面从 RTCP 发送器和 RTCP 接收器别离开展叙述。

RTCP 发送器

​ RTCP 发送器在初始化时即须要晓得本人能够发送哪些类型的 RTCP 分组、如何构建这些 RTCP 分组。其保护一个从 RTCP 分组类型(由 uint32_t 变量标识)到构建函数的映射 std::map<uint32_t, BuilderFunc> builders_。在 RTCP 发送器发送分组之前,向其指明所要发送的分组类型等参数,发送器即可在构建分组、连结成复合分组之后,将 RTCP 分组发送进来。

​ 在新增自定义 RTCP 分组类之后,须要定义相应的分组类型标识、相应的分组构建函数,并在初始化 RTCP 发送器时增加二者的映射关系。

/* modules/rtp_rtcp/include/rtp_rtcp_defines.h */

enum RTCPPacketType : uint32_t {
  // ... ...
  kRtcpXrTargetBitrate = 0x200000,
  kRtcpSvsi = 0x1000000  // SVSI 信息的分组类型标识
};
/* modules/rtp_rtcp/source/rtcp_sender.cc */

std::unique_ptr<rtcp::RtcpPacket> RTCPSender::BuildPLI(const RtcpContext& ctx) {rtcp::Pli* pli = new rtcp::Pli();
  pli->SetSenderSsrc(ssrc_);
  pli->SetMediaSsrc(remote_ssrc_);

  ++packet_type_counter_.pli_packets;

  return std::unique_ptr<rtcp::RtcpPacket>(pli);
}

// 构建 SVSI 分组实例的函数,RtcpPacket 是 Svsi 的间接父类(父类的父类)std::unique_ptr<rtcp::RtcpPacket> RTCPSender::BuildSVSI(const RtcpContext& ctx) {rtcp::Svsi* svsi = new rtcp::Svsi();
  svsi->SetSenderSsrc(ssrc_);
  svsi->SetMediaSsrc(remote_ssrc_);

  return std::unique_ptr<rtcp::RtcpPacket>(svsi);
}
/* modules/rtp_rtcp/source/rtcp_sender.cc */

RTCPSender::RTCPSender(
    bool audio,
    Clock* clock,
    // ... ...
    int report_interval_ms)
    : audio_(audio),
      clock_(clock),
      // ... ...
      last_payload_type_(-1) {
  // ... ...
  
  builders_[kRtcpAnyExtendedReports] = &RTCPSender::BuildExtendedReports;
  // 增加从 SVSI 标识到构建 SVSI 分组函数的映射
  builders_[kRtcpSvsi] = &RTCPSender::BuildSVSI;
}

​ 能够发现,新增自定义 RTCP 信息对于 RTCP 发送器的批改并不大,仅须要减少自定义 RTCP 信息的标识和构建分组的函数,无需波及具体的发送逻辑,这使得代码非常容易扩大。这种可扩展性间接来源于 typedef 关键字、形象继承与发送逻辑的职责单一性:

​ RTCP 发送器应用 typedef 在类中定义了 BuilderFunc 类型,将 BuilderFunc 作为一个指向“以 const RtcpContext& 为参数、返回 unique_ptr<RtcpPacket> 的函数”的指针的别名。这样,所有领有雷同的参数和返回值的函数都能够由 BuilderFunc 指向,能够将 BuilderFunc 看作具备雷同参数和返回值的一类函数。

typedef std::unique_ptr<rtcp::RtcpPacket> (RTCPSender::*BuilderFunc)(const RtcpContext&);

BuilderFunc 的返回值为 rtcp::RtcpPacket 类型的智能指针,rtcp::RtcpPacket 是 RTCP 分组的形象,特定负载 FB 信息 Psfb 类、PLI 信息 Pli 类等均间接或间接继承了 RTCP 分组 RtcpPacket 类。应用 RtcpPacket 将所有类型的 RTCP 分组进行形象,RTCP 发送器无需关注所发送的到底是何种具体类型的 RTCP 分组。

​ 在 RTCP 发送器执行发送逻辑(RTCPSender::SendCompoundRTCP() 函数)时,只管其须要晓得要发送哪些 RTCP 分组,然而并不需要关怀各种类型的 RTCP 分组如何构建,仅需通过 builders_ 获取相应的构建函数,构建、连结、发送即可。这样一来,新增自定义 RTCP 信息就齐全不须要改变 RTCP 发送器的发送逻辑,实现了分组构建和分组发送之间的解耦。

RTCP 接收器

​ RTCP 接收器在初始化时即须要晓得接管到不同类型的 RTCP 分组之后该当告诉谁,故在自定义 RTCP 信息之后须要在 RTCPReceiver 类中新增对应的观察者成员变量。在笔者的实现中,即之前定义的 RtcpSVSIObserver 类变量。

/* modules/rtp_rtcp/source/rtcp_receiver.h */

class RTCPReceiver {
 public:
  // ... ...
    
  void IncomingPacket(const uint8_t* packet, size_t packet_size);

  // ... ...

 private:
  // ... ...

  bool ParseCompoundPacket(const uint8_t* packet_begin,
                           const uint8_t* packet_end,
                           PacketInformation* packet_information);

  void TriggerCallbacksFromRtcpPacket(const PacketInformation& packet_information);

  // ... ...
  RtcpIntraFrameObserver* const rtcp_intra_frame_observer_;
  RtcpSVSIObserver* const rtcp_svsi_observer_;  // SVSI 信息的观察者
  // ... ...
};
/* modules/rtp_rtcp/source/rtcp_receiver.cc */

void RTCPReceiver::IncomingPacket(const uint8_t* packet, size_t packet_size) {if (packet_size == 0) {RTC_LOG(LS_WARNING) << "Incoming empty RTCP packet";
    return;
  }

  PacketInformation packet_information;
  if (!ParseCompoundPacket(packet, packet + packet_size, &packet_information))  // 解析 RTCP 复合分组
    return;
  TriggerCallbacksFromRtcpPacket(packet_information);  // 触发 RTCP 分组对应的回调函数
}

​ RTCP 接收器接管到 RTCP 分组之后解析、触发回调的逻辑十分分明,读者自行浏览代码后该当很容易了解。仅需提及笔者自定义的 SVSI 信息属于应用层 FB 信息,故对 SVSI 信息的解析在 HandlePsfbApp() 函数中解决。

/* modules/rtp_rtcp/source/rtcp_receiver.cc */

void RTCPReceiver::HandlePsfbApp(const CommonHeader& rtcp_block,
                                 PacketInformation* packet_information) {
  // ... ...

  {auto loss_notification = absl::make_unique<rtcp::LossNotification>();
    if (loss_notification->Parse(rtcp_block)) {
      packet_information->packet_type_flags |= kRtcpLossNotification;
      packet_information->loss_notification = std::move(loss_notification);
      return;
    }
  }

  {  // 解析 RTCP 复合分组中是否蕴含 SVSI 信息
    rtcp::Svsi svsi;
    if (svsi.Parse(rtcp_block)) {
      packet_information->packet_type_flags |= kRtcpSvsi;
      return;
    }
  }

  // ... ...
}
/* modules/rtp_rtcp/source/rtcp_receiver.cc */

void RTCPReceiver::TriggerCallbacksFromRtcpPacket(const PacketInformation& packet_information) {
  // ... ...
  if (rtcp_intra_frame_observer_) {RTC_DCHECK(!receiver_only_);
    if ((packet_information.packet_type_flags & kRtcpPli) ||
        (packet_information.packet_type_flags & kRtcpFir)) {if (packet_information.packet_type_flags & kRtcpPli) {RTC_LOG(LS_VERBOSE)
            << "Incoming PLI from SSRC" << packet_information.remote_ssrc;
      } else {RTC_LOG(LS_VERBOSE)
            << "Incoming FIR from SSRC" << packet_information.remote_ssrc;
      }
      rtcp_intra_frame_observer_->OnReceivedIntraFrameRequest(local_ssrc);
    }
  }
  // 如果存在 SVSI 信息观察者且 RTCP 复合分组中蕴含 SVSI 信息,则告诉观察者回调
  if (rtcp_svsi_observer_) {RTC_DCHECK(!receiver_only_);
    if (packet_information.packet_type_flags & kRtcpSvsi) {RTC_LOG(LS_VERBOSE)
          << "Incoming SVSI from SSRC" << packet_information.remote_ssrc;
      rtcp_svsi_observer_->OnReceivedSVSI(local_ssrc);
    }
  }
    
  // ... ...
}

​ 行文至此,自定义 RTCP 信息对于 RTP / RTCP 模块的批改曾经完结了。剩下的只须要思考 RTCP 发送方(即视频接管方)如何调用 RTP / RTCP 模块的接口,以及 RTCP 接管方(即视频发送方)如何实现 RTCP 信息观察者并在 RTP / RTCP 模块初始化时作为参数传递给模块。这些与读者的需要更加严密相干,因而上面仅进行简略叙述。

RTCP 发送方相干批改

​ RTCP 发送方即视频接管方。在准备常识的申请关键帧一节中介绍了解码出错的场景,其中发动关键帧申请的是视频接管方的 VideoReceiveStream 类实例,能够简略地认为发动关键帧申请的终点就是 VideoReceiveStream::RequestKeyFrame() 函数。在笔者的需要中,视频接管方在接管到肯定数量的帧之后向视频发送方发送 SVSI 信息,告诉视频发送方进行视频流传输。“视频接管方接管到了足够的帧”这一事件能够被 VideoReceiveStream 类实例察看到,因而发送 SVSI 信息的终点也能够从 VideoReceiveStream 类开始。

VideoReceiveStream 类间接持有 RTP / RTCP 模块(参考 WebRTC 中的相干实现中的类图的红框局部),对于 RTCP 发送方的批改,仿照 VideoReceiveStream 类调用 RTP / RTCP 模块的 RequestKeyFrame() 的过程即可。

RTCP 接管方相干批改

​ RTCP 接管方即视频发送方。在自定义 RTCP 信息之后,须要在 RTCP 接管方增加解决自定义 RTCP 信息的观察者。为此,考查申请关键帧中 RTCP 接管方相干的实现。在申请关键帧的过程中,RTCP 接收器在收到 PLI 信息之后会告诉观察者,观察者进而令编码器发送关键帧。为了实现上述过程,必须将编码器、观察者和 RTCP 接收器进行关联,WebRTC 中这一关联是在 VideoSendStreamImpl 类的构造函数中实现的。

​ 如上图所示,VideoSendStreamImpl 类领有成员变量:编码器指针 video_stream_encoder_、编码器 RTCP 反馈 encoder_feedback_、RTP 视频发送器指针 rtp_video_sender_EncoderRtcpFeedback 类继承了观察者类,实现了在察看到不同 RTCP 信息之后相应的处理函数。

/* video/encoder_rtcp_feedback.h */

class EncoderRtcpFeedback : public RtcpIntraFrameObserver,  // 继承帧内编码图像观察者
                            public RtcpLossNotificationObserver,
                            public MediaTransportKeyFrameRequestCallback {
 public:
  EncoderRtcpFeedback(Clock* clock,
                      const std::vector<uint32_t>& ssrcs,
                      VideoStreamEncoderInterface* encoder);
  ~EncoderRtcpFeedback() override = default;

  void SetRtpVideoSender(const RtpVideoSenderInterface* rtp_video_sender);

  void OnReceivedIntraFrameRequest(uint32_t ssrc) override;  // 实现了收到帧内编码图像申请的处理函数

  // ... ...

 private:
  // ... ...
  const RtpVideoSenderInterface* rtp_video_sender_;
  VideoStreamEncoderInterface* const video_stream_encoder_;
  // ... ...
};
/* video/encoder_rtcp_feedback.cc */

void EncoderRtcpFeedback::OnReceivedIntraFrameRequest(uint32_t ssrc) {RTC_DCHECK(HasSsrc(ssrc));
  {int64_t now_ms = clock_->TimeInMilliseconds();
    rtc::CritScope lock(&crit_);
    if (time_last_intra_request_ms_ + min_keyframe_send_interval_ms_ > now_ms) {return;}
    time_last_intra_request_ms_ = now_ms;
  }

  // Always produce key frame for all streams.
  video_stream_encoder_->SendKeyFrame();  // 收到帧内编码图像申请后,调用编码器发送关键帧}

​ 能够发现,EncoderRtcpFeedback 类是关联编码器与 RTCP 接收器的重要组件。在笔者的需要中,欲使视频发送方进行视频流传输,间接令编码器进行工作即可,因而能够仿照 EncoderRtcpFeedback 类实现 RtcpIntraFrameObserver 接口,自定义 SVSI 信息观察者 RtcpSVSIObserver 并令 EncoderRtcpFeedback 类实现,再将 EncoderRtcpFeedback 实例通过一系列参数传递置入 RTCP 接收器即可。读者如有不同需要可自行摸索实现之。

总结

​ 本文介绍了在 WebRTC 中如何自定义 RTCP 信息并应用。首先介绍了准备常识,包含 RTCP 分组分类、复合 RTCP 分组、RTCP FB 信息、WebRTC 中申请关键帧所波及的类以及过程等。接着介绍了如何实现自定义 RTCP 信息并应用,包含自定义 RTCP 分组类、批改 RTP / RTCP 模块、批改 RTCP 发送方相干代码、批改 RTCP 接管方相干代码。此外,在行文中交叉了对 WebRTC 代码设计的浅显剖析。本文并未钻研 WebRTC 组件的实现细节,而是着重梳理了组件之间的关系,虽不深刻,然而对于了解 WebRTC 零碎的设计与运行该当还是有帮忙的。

20220414 锎

附录

​ 为寻找不便,记录一些重要的类所在文件的门路(WebRTC 版本:M74,根目录为 webrtc/src):

  1. RTCP 分组类

    • Plimodules/rtp_rtcp/source/rtcp_packet/pli.h
  2. RTP / RTCP 模块

    • RtpRtcpmodules/rtp_rtcp/include/rtp_rtcp.h
    • ModuleRtpRtcpImplmodules/rtp_rtcp/source/rtp_rtcp_impl.h
    • RTCPSendermodules/rtp_rtcp/source/rtcp_sender.h
    • RTCPReceivermodules/rtp_rtcp/source/rtcp_receiver.h
  3. 视频发送方(RTCP 接管方)相干

    • RtpVideoSender / RtpStreamSendercall/rtp_video_sender.h
    • RtpSenderObserverscall/rtp_transport_controller_send_interface.h
    • RtcpIntraFrameObservermodules/rtp_rtcp/include/rtp_rtcp_defines.h
    • EncoderRtcpFeedbackvideo/encoder_rtcp_feedback.h
    • RtpVideoSenderInterfacecall/rtp_video_sender_interface.h
    • VideoSendStreamImplvideo/video_send_stream_impl.h
    • VideoStreamEncoderInterfaceapi/video/video_stream_encoder_interface.h
  4. 视频接管方(RTCP 发送方)相干

    • KeyFrameRequestSendermodules/include/module_common_types.h
    • VideoReceiveStreamvideo/video_receive_stream.h
    • RtpVideoStreamReceivervideo/rtp_video_stream_receiver.h

正文完
 0