认真看完这篇文章,相信你能收获一些东西
webrtc是基于udp协议来进行传输音视频数据的,所以基于udp的特性,rtc采用了2种方式来优化丢包问题
NACK 模块是 WebRTC 对抗弱网的核心 QoS 技术之一,有两种发送模式,一种是基于时间序列的发送模式,一种是基于包序列号的发送模式。很明显,NACK机制,也是需要两端配合进行同时处理,我们分别来讨论下。
当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方
看上去非常简单的一个逻辑,但是我们思考下几个问题
针对现实中网络的复杂程度,上面的问题都是需要我们考虑之内的。
C++音视频开发WebRTC学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
rtc内部也考虑到了这些问题,目前有一些实施的策略来保证,记住几个关键的数字如下。
const int kMaxNackRetries = 10;
const int kProcessIntervalMs = 20;
const int kDefaultRttMs = 100;
const int kMaxNackPackets = 1000;
const int kMaxPacketAge = 10000;
关于第四点,nack_list 的最大长度,这里拉出来单独理解下,如果丢失的包数量超过 1000,会循环清空 nack_list 中关键帧之前的包,直到其长度小于 1000,但是并不是清除到刚好到1000的数量,也就是说,放弃对关键帧首包之前的包的重传请求,直接而快速的以关键帧首包之后的包号作为重传请求的开始。
怎么理解呢?有过音视频相关知识的同学知道,在一个GOP内,解码时,后面的帧都是参考前面的帧进行解码的,如果一个GOP内,前面的帧被清掉了,后面的也没有重传的必要。
举个例子,假如我们接收方,收到的包序号是 1/701/1201,并且都是关键帧的包,那按照上面的算法,我们丢失包是700+500 = 1200个,此时触发了大于1000的条件,那么需要清空超过的包体,按照上面关键帧的算法,那么这里会将701之前的包都会清除掉保证重传的意义。因为如果按照只清除超过的包体算法,只会清除1-201的包,但是如果这样,201-700的包体,重传了也没有意义,因为无法进行解码。
nack_module.h
class NackModule : public Module {
public:
..............
int OnReceivedPacket(uint16_t seq_num, bool is_keyframe);
int OnReceivedPacket(uint16_t seq_num, bool is_keyframe, bool is_recovered);
void ClearUpTo(uint16_t seq_num);
void UpdateRtt(int64_t rtt_ms);
void Clear();
// Module implementation
int64_t TimeUntilNextProcess() override;
void Process() override;
private:
struct NackInfo {
NackInfo();
NackInfo(uint16_t seq_num,
uint16_t send_at_seq_num,
int64_t created_at_time);
uint16_t seq_num;
uint16_t send_at_seq_num;
int64_t created_at_time;
int64_t sent_at_time;
int retries;
};
std::map> nack_list_
RTC_GUARDED_BY(crit_);
std::set> keyframe_list_
RTC_GUARDED_BY(crit_);
std::set> recovered_list_
RTC_GUARDED_BY(crit_);
video_coding::Histogram reordering_histogram_ RTC_GUARDED_BY(crit_);
bool initialized_ RTC_GUARDED_BY(crit_);
int64_t rtt_ms_ RTC_GUARDED_BY(crit_);
uint16_t newest_seq_num_ RTC_GUARDED_BY(crit_);
// Only touched on the process thread.
int64_t next_process_time_ms_;
// Adds a delay before send nack on packet received.
const int64_t send_nack_delay_ms_;
const absl::optional backoff_settings_;
};
} // namespace webrtc
#endif // MODULES_VIDEO_CODING_DEPRECATED_NACK_MODULE_H_
C++音视频开发WebRTC学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
int NackModule2::OnReceivedPacket(uint16_t seq_num,
bool is_keyframe,
bool is_recovered) {
RTC_DCHECK_RUN_ON(worker_thread_);
// TODO(philipel): When the packet includes information whether it is
// retransmitted or not, use that value instead. For
// now set it to true, which will cause the reordering
// statistics to never be updated.
bool is_retransmitted = true;
if (!initialized_) {
newest_seq_num_ = seq_num;
if (is_keyframe)
keyframe_list_.insert(seq_num);
initialized_ = true;
return 0;
}
// Since the |newest_seq_num_| is a packet we have actually received we know
// that packet has never been Nacked.
if (seq_num == newest_seq_num_)
return 0;
if (AheadOf(newest_seq_num_, seq_num)) {
// An out of order packet has been received.
auto nack_list_it = nack_list_.find(seq_num);
int nacks_sent_for_packet = 0;
if (nack_list_it != nack_list_.end()) {
nacks_sent_for_packet = nack_list_it->second.retries;
nack_list_.erase(nack_list_it);
}
if (!is_retransmitted)
UpdateReorderingStatistics(seq_num);
return nacks_sent_for_packet;
}
// Keep track of new keyframes.
if (is_keyframe)
keyframe_list_.insert(seq_num);
// And remove old ones so we don't accumulate keyframes.
auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != keyframe_list_.begin())
keyframe_list_.erase(keyframe_list_.begin(), it);
if (is_recovered) {
recovered_list_.insert(seq_num);
// Remove old ones so we don't accumulate recovered packets.
auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != recovered_list_.begin())
recovered_list_.erase(recovered_list_.begin(), it);
// Do not send nack for packets recovered by FEC or RTX.
return 0;
}
AddPacketsToNack(newest_seq_num_ + 1, seq_num);
newest_seq_num_ = seq_num;
// Are there any nacks that are waiting for this seq_num.
std::vector nack_batch = GetNackBatch(kSeqNumOnly);
if (!nack_batch.empty()) {
// This batch of NACKs is triggered externally; the initiator can
// batch them with other feedback messages.
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);
}
return 0;
}
该函数实现了基于时间周期(20ms)的 nack 发送模式,参考策略 2。具体的处理周期计算方法如下:
void DEPRECATED_NackModule::Process() {
if (nack_sender_) {
std::vector nack_batch;
{
rtc::CritScope lock(&crit_);
nack_batch = GetNackBatch(kTimeOnly);
}
if (!nack_batch.empty()) {
// This batch of NACKs is triggered externally; there is no external
// initiator who can batch them with other feedback messages.
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/false);
}
}
// Update the next_process_time_ms_ in intervals to achieve
// the targeted frequency over time. Also add multiple intervals
// in case of a skip in time as to not make uneccessary
// calls to Process in order to catch up.
int64_t now_ms = clock_->TimeInMilliseconds();
if (next_process_time_ms_ == -1) {
next_process_time_ms_ = now_ms + kProcessIntervalMs;
} else {
next_process_time_ms_ = next_process_time_ms_ + kProcessIntervalMs +
(now_ms - next_process_time_ms_) /
kProcessIntervalMs * kProcessIntervalMs;
}
}
上面说到的策略,rtc内部并不是每隔 20 毫秒批量处理 nack_list,而是动态变化的,这里的算法主要是
int64_t now_ms = clock_->TimeInMilliseconds();
next_process_time_ms_ = next_process_time_ms_ + kProcessIntervalMs +
(now_ms - next_process_time_ms_) /
kProcessIntervalMs * kProcessIntervalMs;
主要是引入了now_ms根据当前处理的时间点进行叠加,这么做的原因是为了应对 cpu 繁忙时线程调度滞后的场景,追赶上正常的处理进度,这就是动态处理周期的意义所在。
C++音视频开发WebRTC学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
std::vector DEPRECATED_NackModule::GetNackBatch(
NackFilterOptions options) {
bool consider_seq_num = options != kTimeOnly;
bool consider_timestamp = options != kSeqNumOnly;
Timestamp now = clock_->CurrentTime();
std::vector nack_batch;
auto it = nack_list_.begin();
while (it != nack_list_.end()) {
bool delay_timed_out =
now.ms() - it->second.created_at_time >= send_nack_delay_ms_;
bool nack_on_rtt_passed =
now.ms() - it->second.sent_at_time >= resend_delay.ms();
bool nack_on_seq_num_passed =
it->second.sent_at_time == -1 &&
AheadOrAt(newest_seq_num_, it->second.send_at_seq_num);
//判断是否满足基于序号或者基于时间序列的条件
if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) ||
(consider_timestamp && nack_on_rtt_passed))) {
nack_batch.emplace_back(it->second.seq_num);
++it->second.retries;
it->second.sent_at_time = now.ms();
if (it->second.retries >= kMaxNackRetries) {
RTC_LOG(LS_WARNING) << "Sequence number " << it->second.seq_num
<< " removed from NACK list due to max retries.";
it = nack_list_.erase(it);
} else {
++it;
}
continue;
}
++it;
}
return nack_batch;
}
该函数传入 nack 过滤选项参数,根据时间或者序列号批量获取 nack_list 中的包序列号,并返回存储了这些包号的数组 nack_batch。
满足条件后,将该 nack_info 中请求重传的包号加入到 nack_batch 数组,重传请求次数 +1,更新 nack 发送时间,如果大于kMaxNackRetries,那么则直接移除即可
文章比较长,大家可以针对源码来对比看下逻辑实现。
留言与评论(共有 0 条评论) “” |