简介
笔者目前在做一个基于 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
、NackSender
、video_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
。在 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 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(pli);
}
// 构建 SVSI 分组实例的函数,RtcpPacket 是 Svsi 的间接父类(父类的父类)
std::unique_ptr RTCPSender::BuildSVSI(const RtcpContext& ctx) {
rtcp::Svsi* svsi = new rtcp::Svsi();
svsi->SetSenderSsrc(ssrc_);
svsi->SetMediaSsrc(remote_ssrc_);
return std::unique_ptr(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
的函数”的指针的别名。这样,所有拥有相同的参数和返回值的函数都可以由 BuilderFunc
指向,可以将 BuilderFunc
看作具有相同参数和返回值的一类函数。
typedef std::unique_ptr (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();
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& 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
):
RTCP 分组类
Pli
-modules/rtp_rtcp/source/rtcp_packet/pli.h
RTP / RTCP 模块
RtpRtcp
-modules/rtp_rtcp/include/rtp_rtcp.h
ModuleRtpRtcpImpl
-modules/rtp_rtcp/source/rtp_rtcp_impl.h
RTCPSender
-modules/rtp_rtcp/source/rtcp_sender.h
RTCPReceiver
-modules/rtp_rtcp/source/rtcp_receiver.h
视频发送方(RTCP 接收方)相关
RtpVideoSender
/RtpStreamSender
-call/rtp_video_sender.h
RtpSenderObservers
-call/rtp_transport_controller_send_interface.h
RtcpIntraFrameObserver
-modules/rtp_rtcp/include/rtp_rtcp_defines.h
EncoderRtcpFeedback
-video/encoder_rtcp_feedback.h
RtpVideoSenderInterface
-call/rtp_video_sender_interface.h
VideoSendStreamImpl
-video/video_send_stream_impl.h
VideoStreamEncoderInterface
-api/video/video_stream_encoder_interface.h
视频接收方(RTCP 发送方)相关
KeyFrameRequestSender
-modules/include/module_common_types.h
VideoReceiveStream
-video/video_receive_stream.h
RtpVideoStreamReceiver
-video/rtp_video_stream_receiver.h