转载请标明出处:https://blog.csdn.net/qq_29621351
WebRTC语音传输中保证传输质量的主要措施分为丢包重传 (NACK) 和前向纠错 (FEC),其中NACK主要是接收端在检测到数据包RTP序列号不连续的时候,发送重传请求,发送端接收到重传请求后重新向接收端发送丢失的数据。FEC主要通过后一个数据包携带前一个数据包的冗余包,这样当前一个数据包丢失时,可以用后一个数据包的冗余数据恢复前一个数据包,降低丢包率的同时,也增大了数据传输的带宽。
WebRTC中音频前向纠错与视频前向纠错的方式不同,音频前向纠错遵循RFC2198标准,视频前向纠错遵循RFC5109标准。两者有差异的原因是音频传输所占据的带宽比较小,即使增加1倍的带宽冗余,也不会造成太大的影响,而视频的一帧比较大,通常需要几个RTP数据包才能完全发送,因此不能像音频一样具有较大的冗余力度。
WebRTC内置的opus音频编码器内置了前向纠错功能,可以在初始化编码器的时候通过接口设置开启,反馈的丢包率没有大但设置开启后在接收端于特定阈值时,opus也不会启用内置的FEC。具体的阈值详见我之前文章的测试结果 编解码器:Opus编码器内置FEC功能测试,如果使用了opus编码,那么就没必要再使用外置的FEC,因为两者的实现方式是一样的。外置的音频前向纠错主要是为了没有内置FEC功能的音频编解码器。
在网络中发送连续的语音包时,假设给每个语音包编号1,2,3,......
如图1所示,语音包按照序号递增的顺序由发送端发出并经过网络传输到达接收端,如果因为网络的不稳定性,使得传输的过程中某个语音包丢失,就会造成播放端播放语音时产生卡顿 (丢失语音)。
WebRTC音频前向纠错采用添加冗余数据的方式,利用后一个语音包携带前一个语音包的冗余数据,当前面的数据包丢失时,解码后面的数据包中的冗余数据用来补偿前面数据包的丢失。
如图2所示,WebRTC语音前向纠错的实现将发送的第一个包单独发送语音包1,第二个包发送语音包2和语音包1,第三个包发送语音包3和语音包2,以此类推。接收端对接收到的每个数据包进行解码,能够解码出两段语音,如果前面的数据包没有丢失则丢弃与前面数据包重复的语音片段,如果丢失则按顺序播放两段语音。这样一来当发生非连续丢包时,就能完全避免丢包带来的声音播放卡顿问题,WebRTC语音前向就错正是基于这种方式。
WebRTC实现音频前向纠错的代码在 /src/modules/audio_coding/codecs/red/ 文件夹 (src为源码文件夹) 下的 audio_encoder_copy_red.cc 文件中实现,其头文件为 audio_encoder_copy_red.h。在源文件的EncodeImpl函数中实现,实现代码如下
AudioEncoder::EncodedInfo AudioEncoderCopyRed::EncodeImpl(
uint32_t rtp_timestamp,
rtc::ArrayView audio,
rtc::Buffer* encoded)
{
const size_t primary_offset = encoded->size();
EncodedInfo info = speech_encoder_->Encode(rtp_timestamp, audio, encoded);
RTC_CHECK(info.redundant.empty()) << "Cannot use nested redundant encoders.";
RTC_DCHECK_EQ(encoded->size() - primary_offset, info.encoded_bytes);
if (info.encoded_bytes > 0)
{
// |info| will be implicitly cast to an EncodedInfoLeaf struct, effectively
// discarding the (empty) vector of redundant information. This is
// intentional.
info.redundant.push_back(info);
RTC_DCHECK_EQ(info.redundant.size(), 1);
if (secondary_info_.encoded_bytes > 0)
{
encoded->AppendData(secondary_encoded_);
info.redundant.push_back(secondary_info_);
RTC_DCHECK_EQ(info.redundant.size(), 2);
}
// Save primary to secondary.
secondary_encoded_.SetData(encoded->data() + primary_offset,
info.encoded_bytes);
secondary_info_ = info;
RTC_DCHECK_EQ(info.speech, info.redundant[0].speech);
}
// Update main EncodedInfo.
info.payload_type = red_payload_type_;
info.encoded_bytes = 0;
for (std::vector::const_iterator it = info.redundant.begin();
it != info.redundant.end(); ++it)
{
info.encoded_bytes += it->encoded_bytes;
}
return info;
}
代码中定义的AudioEncoderCopyRed类包含4个私有成员变量,分别是
// the underlying AudioEncoder object that performs the actual encodings.
std::unique_ptr speech_encoder_;
// redundant payload type
int red_payload_type_;
// the buffer which hold the redundant audio data
rtc::Buffer secondary_encoded_;
// information about the redundant data
EncodedInfoLeaf secondary_info_;
speech_encoder_ 表示执行实际编码的音频编码器,red_payload_type_ 表示冗余包的负载类型,secondary_encoded_ 表示存放冗余音频数据的缓存空间,secondary_info_ 表示冗余编码的编码信息。EncodedInfo 表示编码信息,定义如下。
struct EncodedInfoLeaf
{
size_t encoded_bytes = 0;
uint32_t encoded_timestamp = 0;
int payload_type = 0;
bool send_even_if_empty = false;
bool speech = true;
CodecType encoder_type = CodecType::kOther;
};
// This is the main struct for auxiliary encoding information. Each encoded
// packet should be accompanied by one EncodedInfo struct, containing the
// total number of |encoded_bytes|, the |encoded_timestamp| and the
// |payload_type|. If the packet contains redundant encodings, the |redundant|
// vector will be populated with EncodedInfoLeaf structs. Each struct in the
// vector represents one encoding; the order of structs in the vector is the
// same as the order in which the actual payloads are written to the byte
// stream. When EncoderInfoLeaf structs are present in the vector, the main
// struct's |encoded_bytes| will be the sum of all the |encoded_bytes| in the
// vector.
struct EncodedInfo : public EncodedInfoLeaf
{
EncodedInfo();
EncodedInfo(const EncodedInfo&);
EncodedInfo(EncodedInfo&&);
~EncodedInfo();
EncodedInfo& operator=(const EncodedInfo&);
EncodedInfo& operator=(EncodedInfo&&);
std::vector redundant;
};
EncodedInfo 包含一个完整的主编码信息和一个冗余编码信息的数组。从EncodeImpl函数的实现源码中可以看到,WebRTC将前一个包的冗余信息完全复制到后面一个包。EncodeImpl函数实现的程序流程如图3。
程序流程图对应着WebRTC音频前向纠错的处理过程。感兴趣的话可以对应 图3 肉眼debug一下代码。以上就是WebRTC实现音频前向纠错的方式。WebRTC音频前向纠错策略的使用很简单易懂,仅仅是增加了1倍的冗余。策略符合RFC2198,但只使用了RFC2198标准能提供的最简单的方式。那么WebRTC为什么要使用这样简单的方式?其实我也不太知道。