(01)Webrtc::Fec与Nack的二三事

2024-04-01 17:32
文章标签 01 二三 webrtc fec nack

本文主要是介绍(01)Webrtc::Fec与Nack的二三事,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  写在前面:要理解Fec与Nack逻辑,我喜欢先从接受端看, 理解了Fec与Nack是如何被使用的,才能更好的明白不同的机制应该怎么用,在什么场合用。

 

  更新丢包逻辑

void PacketBuffer::UpdateMissingPackets(uint16_t seq_num) {if (!newest_inserted_seq_num_)newest_inserted_seq_num_ = seq_num;const int kMaxPaddingAge = 1000;if (AheadOf(seq_num, *newest_inserted_seq_num_)) {uint16_t old_seq_num = seq_num - kMaxPaddingAge;auto erase_to = missing_packets_.lower_bound(old_seq_num);missing_packets_.erase(missing_packets_.begin(), erase_to);// Guard against inserting a large amount of missing packets if there is a// jump in the sequence number.if (AheadOf(old_seq_num, *newest_inserted_seq_num_)) {*newest_inserted_seq_num_ = old_seq_num;}++*newest_inserted_seq_num_;while (AheadOf(seq_num, *newest_inserted_seq_num_)) {missing_packets_.insert(*newest_inserted_seq_num_);++*newest_inserted_seq_num_;}} else {missing_packets_.erase(seq_num);}
}

  1.AheadOf(A, B)可以简单理解为A>B。实际环形数据的比较与环形存储空间size有关,相当与(A > B) xor (abs(A - B) > size / 2)。

  2.首先判断当前包序号seq_num是否在之前插入的包序号newest_inserted_seq_num之后,如果不是,证明是之前的丢包,走else逻辑。如果是,则开始判断是否有丢包。

  3.删除与当前包序号相差超过1000的丢包序号,因为太老了。如果seq_num - 1000 > newest_inserted_seq_num,证明中间丢包太多,只从seq_num - 1000开始插入丢包序号。

  4.newest_inserted_seq_num到seq_num之间的全部包被认定为丢包。

举例:收包1000, 1001, 1002, 1005,1003……收到1005时认定1003/1004丢包,收到1003时删除丢包列表中的1003,只剩1004。

注意:packet_buffer的missing_packets_在webrtc中虽然做了更新,但除了H264判断IPPP依赖是否有丢依赖帧之外,并没有被使用,所以这里其实并不影响实际的网络体验。

 

  判断收到数据包时是否可能组成一帧

bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {size_t index = seq_num % buffer_.size();int prev_index = index > 0 ? index - 1 : buffer_.size() - 1;const auto& entry = buffer_[index];const auto& prev_entry = buffer_[prev_index];if (entry == nullptr)return false;if (entry->seq_num != seq_num)return false;if (entry->is_first_packet_in_frame())return true;if (prev_entry == nullptr)return false;if (prev_entry->seq_num != static_cast<uint16_t>(entry->seq_num - 1))return false;if (prev_entry->timestamp != entry->timestamp)return false;if (prev_entry->continuous)return true;return false;
}

  判断是否可能组成一个完整帧的条件有两个:1.该包为帧的地一个包。2.包连续,前一包存在且时间戳相同。可以看到,这里用于判断的是buffer_的实际数据,而非missing_packets_。buffer_在执行PacketBuffer::InsertPacket时被更新。

  注意这里的continuous,有两个位置会设置这个值:InsertPacket的时候被置为false,或是通过PotentialNewFrame检测后在FindFrames中被置为true。也就是说,一个包的continuous置为true,唯一的途径为通过PotentialNewFrame检测。于是一帧里的第二个包被置为true,依赖于收到第一个包,走is_first_packet_in_frame() == true分支,而第N个包被置为true依赖于第N-1个包被置为true。所以一个包的上一个包的continuous标记为true,就表示这个包所属帧,从第一个包到当前包都是连续的。

 

  寻找完整帧

std::vector<std::unique_ptr<PacketBuffer::Packet>> PacketBuffer::FindFrames(uint16_t seq_num) {std::vector<std::unique_ptr<PacketBuffer::Packet>> found_frames;for (size_t i = 0; i < buffer_.size() && PotentialNewFrame(seq_num); ++i) {size_t index = seq_num % buffer_.size();buffer_[index]->continuous = true;// If all packets of the frame is continuous, find the first packet of the// frame and add all packets of the frame to the returned packets.if (buffer_[index]->is_last_packet_in_frame()) {uint16_t start_seq_num = seq_num;// Find the start index by searching backward until the packet with// the |frame_begin| flag is set.int start_index = index;size_t tested_packets = 0;int64_t frame_timestamp = buffer_[start_index]->timestamp;// Identify H.264 keyframes by means of SPS, PPS, and IDR.bool is_h264 = buffer_[start_index]->codec() == kVideoCodecH264;bool has_h264_sps = false;bool has_h264_pps = false;bool has_h264_idr = false;bool is_h264_keyframe = false;int idr_width = -1;int idr_height = -1;while (true) {++tested_packets;if (!is_h264 && buffer_[start_index]->is_first_packet_in_frame())break;if (is_h264) {// ture: RTC_LOG(LS_ERROR) << "kiki-h264";const auto* h264_header = absl::get_if<RTPVideoHeaderH264>(&buffer_[start_index]->video_header.video_type_header);if (!h264_header || h264_header->nalus_length >= kMaxNalusPerPacket)return found_frames;for (size_t j = 0; j < h264_header->nalus_length; ++j) {if (h264_header->nalus[j].type == H264::NaluType::kSps) {has_h264_sps = true;} else if (h264_header->nalus[j].type == H264::NaluType::kPps) {has_h264_pps = true;} else if (h264_header->nalus[j].type == H264::NaluType::kIdr) {has_h264_idr = true;}}if ((sps_pps_idr_is_h264_keyframe_ && has_h264_idr && has_h264_sps &&has_h264_pps) ||(!sps_pps_idr_is_h264_keyframe_ && has_h264_idr)) {is_h264_keyframe = true;// Store the resolution of key frame which is the packet with// smallest index and valid resolution; typically its IDR or SPS// packet; there may be packet preceeding this packet, IDR's// resolution will be applied to them.if (buffer_[start_index]->width() > 0 &&buffer_[start_index]->height() > 0) {idr_width = buffer_[start_index]->width();idr_height = buffer_[start_index]->height();}}}if (tested_packets == buffer_.size())break;start_index = start_index > 0 ? start_index - 1 : buffer_.size() - 1;// In the case of H264 we don't have a frame_begin bit (yes,// |frame_begin| might be set to true but that is a lie). So instead// we traverese backwards as long as we have a previous packet and// the timestamp of that packet is the same as this one. This may cause// the PacketBuffer to hand out incomplete frames.// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=7106if (is_h264 && (buffer_[start_index] == nullptr ||buffer_[start_index]->timestamp != frame_timestamp)) {break;}--start_seq_num;}if (is_h264) {// Warn if this is an unsafe frame.if (has_h264_idr && (!has_h264_sps || !has_h264_pps)) {RTC_LOG(LS_WARNING)<< "Received H.264-IDR frame ""(SPS: "<< has_h264_sps << ", PPS: " << has_h264_pps << "). Treating as "<< (sps_pps_idr_is_h264_keyframe_ ? "delta" : "key")<< " frame since WebRTC-SpsPpsIdrIsH264Keyframe is "<< (sps_pps_idr_is_h264_keyframe_ ? "enabled." : "disabled");}// Now that we have decided whether to treat this frame as a key frame// or delta frame in the frame buffer, we update the field that// determines if the RtpFrameObject is a key frame or delta frame.const size_t first_packet_index = start_seq_num % buffer_.size();if (is_h264_keyframe) {buffer_[first_packet_index]->video_header.frame_type =VideoFrameType::kVideoFrameKey;if (idr_width > 0 && idr_height > 0) {// IDR frame was finalized and we have the correct resolution for// IDR; update first packet to have same resolution as IDR.buffer_[first_packet_index]->video_header.width = idr_width;buffer_[first_packet_index]->video_header.height = idr_height;}} else {buffer_[first_packet_index]->video_header.frame_type =VideoFrameType::kVideoFrameDelta;}// With IPPP, if this is not a keyframe, make sure there are no gaps// in the packet sequence numbers up until this point.const uint8_t h264tid =buffer_[start_index] != nullptr? buffer_[start_index]->video_header.frame_marking.temporal_id: kNoTemporalIdx;if (h264tid == kNoTemporalIdx && !is_h264_keyframe &&missing_packets_.upper_bound(start_seq_num) !=missing_packets_.begin()) {return found_frames;}}const uint16_t end_seq_num = seq_num + 1;// Use uint16_t type to handle sequence number wrap around case.uint16_t num_packets = end_seq_num - start_seq_num;found_frames.reserve(found_frames.size() + num_packets);for (uint16_t i = start_seq_num; i != end_seq_num; ++i) {std::unique_ptr<Packet>& packet = buffer_[i % buffer_.size()];RTC_DCHECK(packet);RTC_DCHECK_EQ(i, packet->seq_num);// Ensure frame boundary flags are properly set.packet->video_header.is_first_packet_in_frame = (i == start_seq_num);packet->video_header.is_last_packet_in_frame = (i == seq_num);found_frames.push_back(std::move(packet));}missing_packets_.erase(missing_packets_.begin(),missing_packets_.upper_bound(seq_num));}++seq_num;}return found_frames;
}

  1.正常网络条件下的视频包,都会通过PotentialNewFrame检测,然后因为不是帧的最后一包,直接seq_num++,下一次循环entry == nullptr而退出循环。seq_num++的原因是,新插入的包可能是帧间丢失的最后一包,例如一帧由1000-1006包组成,只有1003包丢失,则收到1003包时会首先通过PotentialNewFrame检测,然后因为不是帧的最后一包,不断seq_num++,将1004/1005/1006包的continuous标志置为true,并从1006包开始组帧搜索。

  2.如果是帧的最后一包,开始反向寻找帧的第一包。对于h264,判断方式为a.前一包为空,b.前一包的时间戳不同。注释说h264的frame begin位是假的,所以不用改标志作为判断。不是很理解,实测标志位是有效且准确的。标志位通过video_rtp_depacketizer_h264的ParseFuaNalu函数,first_fragment = (rtp_payload.cdata()[1] & kSBit) > 0判断。

// In the case of H264 we don't have a frame_begin bit (yes,
// |frame_begin| might be set to true but that is a lie). So instead
// we traverese backwards as long as we have a previous packet and
// the timestamp of that packet is the same as this one. This may cause
// the PacketBuffer to hand out incomplete frames.
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=7106

  3.对于h264,如果missing_packets_.upper_bound(start_seq_num) != missing_packets_.begin()并且当前不是I帧,说明当前虽然能组成P帧,但IPPP依赖缺失,直接返回。一个问题是导致卡顿,因为当前画面本来是可以播放的,只是有可能花屏。另一个问题是导致丢帧,因为当前帧所有包已经收齐,接下来再收的包都不会触发这一帧的组帧搜索,而这一帧本身并没有被返回。(前面说的不对,FindFrames返回不止一帧,当前面的依赖帧收全后,会触发这一帧的返回)这里可以优化,即使前面P帧缺失也返回当前帧,但可能会导致部分花屏,特别对于GOP20秒且网络不好的情况,可能会一直在花屏状态

  4.从日志来看,h264收到的每个I帧都是IDR,包含SPS/PPS。

  5.nalus_length代表包里包含几帧,对于视频来说,h264为fu-a分包,即多个包组合一个帧。直到一帧的起始包,才会看到nalus_length为1,其它为0。

  6.h264tid一直是kNoTemporalIdx,不太理解这是什么标志。

  7.将可以组帧的所有包返回,并更新missing_packets_,既然已经组帧,那么seq_num之前的丢包全部不要了。这里很重要,如果没有这个,做了第3条红字部分优化,可能会导致退帧现象。std::move会清空buffer_对应位置数据。

 

  NACK

bool NackModule::RemovePacketsUntilKeyFrame() {while (!keyframe_list_.empty()) {auto it = nack_list_.lower_bound(*keyframe_list_.begin());if (it != nack_list_.begin()) {// We have found a keyframe that actually is newer than at least one// packet in the nack list.nack_list_.erase(nack_list_.begin(), it);return true;}// If this keyframe is so old it does not remove any packets from the list,// remove it from the list of keyframes and try the next keyframe.keyframe_list_.erase(keyframe_list_.begin());}return false;
}void NackModule::AddPacketsToNack(uint16_t seq_num_start,uint16_t seq_num_end) {// Remove old packets.auto it = nack_list_.lower_bound(seq_num_end - kMaxPacketAge);nack_list_.erase(nack_list_.begin(), it);// If the nack list is too large, remove packets from the nack list until// the latest first packet of a keyframe. If the list is still too large,// clear it and request a keyframe.uint16_t num_new_nacks = ForwardDiff(seq_num_start, seq_num_end);if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {while (RemovePacketsUntilKeyFrame() &&nack_list_.size() + num_new_nacks > kMaxNackPackets) {}if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {nack_list_.clear();keyframe_request_sender_->RequestKeyFrame();return;}}for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {// Do not send nack for packets that are already recovered by FEC or RTXif (recovered_list_.find(seq_num) != recovered_list_.end())continue;NackInfo nack_info(seq_num, seq_num + WaitNumberOfPackets(0.5),clock_->TimeInMilliseconds());RTC_DCHECK(nack_list_.find(seq_num) == nack_list_.end());nack_list_[seq_num] = nack_info;}
}

  1.nack_list_维护丢包seq_num,keyframe_list_维护已收到的I帧seq_num。

  2.当nacks数目超过kMaxNackPackets,首先不断调用RemovePacketUntilKeyFrame,指导返回结果为false或者数目不再超过kMaxNackPackets。

  3.RemovePacketUntilKeyFrame逻辑:

      a.如果nack_list_中有早于keyframe_list_最早关键帧的seq_num,删除这些包序号直到最早关键帧seq_num,返回true。

      b.如不符合条件a,删除最早关键帧seq_num,并再次进行判断。

      c.当删光keyframe_list_后返回false。

  4.如果删光keyframe_list_后,nacks数目依然超标,清空nack_list_,并请求重发关键帧。

  5.循环每一个丢包的seq_num,由seq_num生成NackInfo信息,插入nack_list_中。注意因为在NACK的OnReceivedPacket函数中,写死了”bool is_retransmitted = true;“,不会调用UpdateReorderingStatistics,所以WaitNumberOfPackets一直返回0。NackInfo的seq_num与send_at_seq_num均为丢包的seq_num。

  6.可以看到,在逻辑中,如果包已经被fec恢复,就不会加入nack_list_列表中。但因为ulpfec传输视频包与fec包的顺序,大部分情况下丢包检测先于fec包到达,这里实际没什么作用。

 

  收包的NACK回调

int NackModule::OnReceivedPacket(uint16_t seq_num,bool is_keyframe,bool is_recovered) {rtc::CritScope lock(&crit_);// 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<uint16_t> 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;
}

  1.当新收到的seq_num存在于nack_list_中,表明这个包已经收到,不需要重传,从nack_list_中移除。返回这个包被重传次数。

  2.is_keyframe表示收到的包是否输入关键帧,is_recovered表示收到的包是否是从fec恢复回来的,分别处理keyframe_list_及recovered_list_。如果是恢复包,不触发丢包更新直接返回。

  3.AddPacketsToNack更新丢包列表。

std::vector<uint16_t> NackModule::GetNackBatch(NackFilterOptions options) {bool consider_seq_num = options != kTimeOnly;bool consider_timestamp = options != kSeqNumOnly;Timestamp now = clock_->CurrentTime();std::vector<uint16_t> nack_batch;auto it = nack_list_.begin();while (it != nack_list_.end()) {TimeDelta resend_delay = TimeDelta::Millis(rtt_ms_);if (backoff_settings_) {resend_delay =std::max(resend_delay, backoff_settings_->min_retry_interval);if (it->second.retries > 1) {TimeDelta exponential_backoff =std::min(TimeDelta::Millis(rtt_ms_), backoff_settings_->max_rtt) *std::pow(backoff_settings_->base, it->second.retries - 1);resend_delay = std::max(resend_delay, exponential_backoff);}}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;
}

  4.GetNackBatch函数对于nack_list_中满足一定条件的包,将其加入nack_batch一并返回,如果重传次数过多,从nack_list_中移除。这个函数与SendNack有两种被触发的机制,一是每次nack module收到包的回调(NackModule::OnReceivedPacket),模式为kSeqNumOnly,二是nack module轮询,每隔20ms一次(NackModule::Process),模式为kTimeOnly。值得注意的是,NackInfo的sent_at_time并不是-1,所以轮询方式只会根据delay_timed_out成立执行逻辑。限制条件的目的大概是为了防止前一个nack重传发送端还没处理完就又发了新的,同一个seq_num明明发一次就可以解决但被发了多次。

  5.SendNack调用:RtpVideoStreamReceiver::RtcpFeedbackBuffer::SendNack。这个函数首先将batch中的seq_num加入nack_sequence_numbers_中,然后根据buffering_allowed决定是否发送nack包。所以在回调中,只会做插入操作而不会发送nack包,只有在轮询中才会实际发送nack的rtcp请求。这里也可以优化,将OnReceivedPacket中的buffering_allowed改为true,以保证重传/组帧更快的发生,降低延迟。SendBufferedRtcpFeedback -> VideoReceiveStream::SendNack -> RtpVideoStreamReceiver::RequestPacketRetransmit -> ModuleRtpRtcpImpl::SendNack发送对应的rtcp包。

  6.补充:rtp_video_stream_receiver.cc初始化rtcp_feedback_buffer_的KeyFrameRequestSender及LossNotificationSender为自己,而NackSender为video_receive_stream.cc的VideoReceiveStream类。绕的要命,没理解这个设计模式。

 

  FEC

  1.fec维护两个重要队列,recovered_packets及received_fec_packets_。recovered_packets里保存收到的视频包,以及被fec恢复的包。received_fec_packets_里保存收到的fec包,每个fec包还包含指向所保护数据包的指针。

void ForwardErrorCorrection::DecodeFec(const ReceivedPacket& received_packet,RecoveredPacketList* recovered_packets) {RTC_DCHECK(recovered_packets);const size_t max_media_packets = fec_header_reader_->MaxMediaPackets();if (recovered_packets->size() == max_media_packets) {const RecoveredPacket* back_recovered_packet =recovered_packets->back().get();if (received_packet.ssrc == back_recovered_packet->ssrc) {const unsigned int seq_num_diff =MinDiff(received_packet.seq_num, back_recovered_packet->seq_num);if (seq_num_diff > max_media_packets) {// A big gap in sequence numbers. The old recovered packets// are now useless, so it's safe to do a reset.RTC_LOG(LS_INFO) << "Big gap in media/ULPFEC sequence numbers. No need ""to keep the old packets in the FEC buffers, thus ""resetting them.";ResetState(recovered_packets);}}}InsertPacket(received_packet, recovered_packets);AttemptRecovery(recovered_packets);
}

  2.首先做一些过期检测,如果当前的包的seq_num与最后一个收到或恢复的视频包的seq_num差距超过了fec所保护的数据包数量上限,说明之前的包无论如何不可能被恢复了,清空recovered_packets及received_fec_packets_。

void ForwardErrorCorrection::InsertPacket(const ReceivedPacket& received_packet,RecoveredPacketList* recovered_packets) {if (!received_fec_packets_.empty() &&received_packet.ssrc == received_fec_packets_.front()->ssrc) {auto it = received_fec_packets_.begin();while (it != received_fec_packets_.end()) {uint16_t seq_num_diff = MinDiff(received_packet.seq_num, (*it)->seq_num);if (seq_num_diff > 0x3fff) {it = received_fec_packets_.erase(it);} else {// No need to keep iterating, since |received_fec_packets_| is sorted.break;}}}if (received_packet.is_fec) {InsertFecPacket(*recovered_packets, received_packet);} else {InsertMediaPacket(recovered_packets, received_packet);}DiscardOldRecoveredPackets(recovered_packets);
}

  3.对于fec包,a.通过mask解析保护的所有视频包序列号。b.如果序列号存在于recovered_packets中,将对应的指针指向数据包。c.将fec包放入received_fec_packets_。

  4.对于视频包,a.首先放入recovered_packet。b.对于received_fec_packets_中每一个fec包,如果保护的数据包包含该视频包,更新指针。

  5.丢弃用不到的数据包:例如fec最多只保护48个包(看协议,应该最多只能保护24个包,不知道这里为什么设置为48,long mask是1:48bits mask),而recovered_packets中包数超过这个阈值,多出来的显然不可能被用到。

void ForwardErrorCorrection::AttemptRecovery(RecoveredPacketList* recovered_packets) {auto fec_packet_it = received_fec_packets_.begin();while (fec_packet_it != received_fec_packets_.end()) {int packets_missing = NumCoveredPacketsMissing(**fec_packet_it);if (packets_missing == 1) {std::unique_ptr<RecoveredPacket> recovered_packet(new RecoveredPacket());recovered_packet->pkt = nullptr;if (!RecoverPacket(**fec_packet_it, recovered_packet.get())) {fec_packet_it = received_fec_packets_.erase(fec_packet_it);continue;}auto* recovered_packet_ptr = recovered_packet.get();recovered_packets->push_back(std::move(recovered_packet));recovered_packets->sort(SortablePacket::LessThan());UpdateCoveringFecPackets(*recovered_packet_ptr);DiscardOldRecoveredPackets(recovered_packets);fec_packet_it = received_fec_packets_.erase(fec_packet_it);fec_packet_it = received_fec_packets_.begin();} else if (packets_missing == 0) {fec_packet_it = received_fec_packets_.erase(fec_packet_it);} else {fec_packet_it++;}}
}

  6.对于received_fec_packets_中的每一个fec包,首先确认其所保护的包里有多少丢包,判断方式即查看每一个保护包的指针是否为空。通过xor方式只能恢复一个丢包,例如:a xor b xor c = fec -> a = b xor c xor fec。所以如果丢包数超过1个,无法恢复丢包,如果丢包数为0,则fec所保护的所有包均已收到或恢复,fec包不再有意义,丢弃。只有丢包数为1时,通过xor恢复数据包,并将恢复的包,放入recovered_packets中。

 

  FEC解包逻辑

  1.RED包会调用两次ReceivePacket,第一次解析剥离RED头。

void RtpVideoStreamReceiver::NotifyReceiverOfEmptyPacket(uint16_t seq_num) {{rtc::CritScope lock(&reference_finder_lock_);reference_finder_->PaddingReceived(seq_num);}OnInsertedPacket(packet_buffer_.InsertPadding(seq_num));if (nack_module_) {nack_module_->OnReceivedPacket(seq_num, /* is_keyframe = */ false,/* is _recovered = */ false);}if (loss_notification_controller_) {// TODO(bugs.webrtc.org/10336): Handle empty packets.RTC_LOG(LS_WARNING)<< "LossNotificationController does not expect empty packets.";}
}PacketBuffer::InsertResult PacketBuffer::InsertPadding(uint16_t seq_num) {PacketBuffer::InsertResult result;rtc::CritScope lock(&crit_);UpdateMissingPackets(seq_num);result.packets = FindFrames(static_cast<uint16_t>(seq_num + 1));return result;
}

  2.Fec包会触发NotifyReceiverOfEmptyPacket函数。代码注释给的解释是“Notify video_receiver about received FEC packets to avoid NACKing these packets.”。

其中InsertPadding主要做几件事:更新missing_packets_,以及看有没有可能组帧。注意这里传入FindFrames里的是seq_num+1,无论后一个包是新一帧的起始包还是另一个fec包,都不可能找到完整帧(如果是新一帧并且收到全部包,则早已返回,这里PotentialNewFrame也只会entry == nullptr),不是很理解实际意义。OnInsertedPacket会调用AssembleFrame,将属于同一帧的所有包组成这里results.packets为空,当然也不会有操作。这里还有一件重要的事,调用RtpFrameReferenceFinder::PaddingReceived,这个RtpFrameReferenceFinder是用于判断帧依赖的,并且在引入fec后,对于h264的依赖判断有问题,后面仔细分析一下。另外调用nack的收包回调,更新丢包列表。从这里看,padding包/fec包与普通包在nack模块看来,都是一样的,所以fec包丢失也会重传。

  Note: 基于这个原因,Ulpfec一定程度会恶化网页版的体验,因为无法在接收端做对应优化。解决办法是使用独立的传输通路与独立的seq_num,即flexfec。

bool UlpfecReceiverImpl::AddReceivedRedPacket(const RtpPacketReceived& rtp_packet,uint8_t ulpfec_payload_type) {if (rtp_packet.Ssrc() != ssrc_) {RTC_LOG(LS_WARNING)<< "Received RED packet with different SSRC than expected; dropping.";return false;}if (rtp_packet.size() > IP_PACKET_SIZE) {RTC_LOG(LS_WARNING) << "Received RED packet with length exceeds maximum IP ""packet size; dropping.";return false;}rtc::CritScope cs(&crit_sect_);static constexpr uint8_t kRedHeaderLength = 1;if (rtp_packet.payload_size() == 0) {RTC_LOG(LS_WARNING) << "Corrupt/truncated FEC packet.";return false;}// Remove RED header of incoming packet and store as a virtual RTP packet.auto received_packet =std::make_unique<ForwardErrorCorrection::ReceivedPacket>();received_packet->pkt = new ForwardErrorCorrection::Packet();// Get payload type from RED header and sequence number from RTP header.uint8_t payload_type = rtp_packet.payload()[0] & 0x7f;received_packet->is_fec = payload_type == ulpfec_payload_type;received_packet->is_recovered = rtp_packet.recovered();received_packet->ssrc = rtp_packet.Ssrc();received_packet->seq_num = rtp_packet.SequenceNumber();// RTC_LOG(LS_ERROR) << "kiki-fec/packet " << received_packet->seq_num << " is fec/recovered? " << received_packet->is_fec << " " << received_packet->is_recovered;if (rtp_packet.payload()[0] & 0x80) {// f bit set in RED header, i.e. there are more than one RED header blocks.// WebRTC never generates multiple blocks in a RED packet for FEC.RTC_LOG(LS_WARNING) << "More than 1 block in RED packet is not supported.";return false;}++packet_counter_.num_packets;packet_counter_.num_bytes += rtp_packet.size();if (packet_counter_.first_packet_time_ms == -1) {packet_counter_.first_packet_time_ms = rtc::TimeMillis();}if (received_packet->is_fec) {++packet_counter_.num_fec_packets;// everything behind the RED headerreceived_packet->pkt->data =rtp_packet.Buffer().Slice(rtp_packet.headers_size() + kRedHeaderLength,rtp_packet.payload_size() - kRedHeaderLength);} else {auto red_payload = rtp_packet.payload().subview(kRedHeaderLength);received_packet->pkt->data.EnsureCapacity(rtp_packet.headers_size() +red_payload.size());// Copy RTP header.received_packet->pkt->data.SetData(rtp_packet.data(),rtp_packet.headers_size());// Set payload type.received_packet->pkt->data[1] &= 0x80;          // Reset RED payload type.received_packet->pkt->data[1] += payload_type;  // Set media payload type.// Copy payload data.received_packet->pkt->data.AppendData(red_payload.data(),red_payload.size());}if (received_packet->pkt->data.size() > 0) {received_packets_.push_back(std::move(received_packet));}return true;
}

  3.AddReceivedRedPacket函数处理掉RED头,然后将解RED后的包放入received_packets_队列中。

int32_t UlpfecReceiverImpl::ProcessReceivedFec() {crit_sect_.Enter();// If we iterate over |received_packets_| and it contains a packet that cause// us to recurse back to this function (for example a RED packet encapsulating// a RED packet), then we will recurse forever. To avoid this we swap// |received_packets_| with an empty vector so that the next recursive call// wont iterate over the same packet again. This also solves the problem of// not modifying the vector we are currently iterating over (packets are added// in AddReceivedRedPacket).std::vector<std::unique_ptr<ForwardErrorCorrection::ReceivedPacket>>received_packets;received_packets.swap(received_packets_);// RTC_LOG(LS_ERROR) << "kiki-fec/size " << received_packets.size();for (const auto& received_packet : received_packets) {// Send received media packet to VCM.if (!received_packet->is_fec) {ForwardErrorCorrection::Packet* packet = received_packet->pkt;crit_sect_.Leave();recovered_packet_callback_->OnRecoveredPacket(packet->data.data(),packet->data.size());crit_sect_.Enter();// Create a packet with the buffer to modify it.RtpPacketReceived rtp_packet;const uint8_t* const original_data = packet->data.cdata();if (!rtp_packet.Parse(packet->data)) {RTC_LOG(LS_WARNING) << "Corrupted media packet";} else {rtp_packet.IdentifyExtensions(extensions_);// Reset buffer reference, so zeroing would work on a buffer with a// single reference.packet->data = rtc::CopyOnWriteBuffer(0);rtp_packet.ZeroMutableExtensions();packet->data = rtp_packet.Buffer();// Ensure that zeroing of extensions was done in place.RTC_DCHECK_EQ(packet->data.cdata(), original_data);}}if (!received_packet->is_recovered) {// Do not pass recovered packets to FEC. Recovered packet might have// different set of the RTP header extensions and thus different byte// representation than the original packet, That will corrupt// FEC calculation.fec_->DecodeFec(*received_packet, &recovered_packets_);}}// Send any recovered media packets to VCM.for (const auto& recovered_packet : recovered_packets_) {// RTC_LOG(LS_ERROR) << "kiki-fec/recovered: " << recovered_packet->seq_num;if (recovered_packet->returned) {// Already sent to the VCM and the jitter buffer.continue;}ForwardErrorCorrection::Packet* packet = recovered_packet->pkt;++packet_counter_.num_recovered_packets;// Set this flag first; in case the recovered packet carries a RED// header, OnRecoveredPacket will recurse back here.recovered_packet->returned = true;crit_sect_.Leave();recovered_packet_callback_->OnRecoveredPacket(packet->data.data(),packet->data.size());crit_sect_.Enter();}crit_sect_.Leave();return 0;
}

  4.ProcessReceivedFec首先对每个处于received_packets_队列中的包进行处理(从代码来看,因为received_packets_只有在前述AddReceivedRedPacket会做插入操作,而前述函数每次只插入一个包,故这里实际每次只处理一个包)。对于视频包,会先调第二次的ReceivePacket,并最终调用OnReceivedPayloadData,以保证视频帧的尽快渲染。接着对于所有包(无论视频包还是fec包的is_recovered都是false),会做前述fec处理逻辑,尝试恢复丢包。最后循环recovered_packets_队列,如果有returned为false的包,说明是被fec恢复的丢包,如普通视频包一样调用第二次ReceivePacket。

void RtpVideoStreamReceiver::OnRecoveredPacket(const uint8_t* rtp_packet,size_t rtp_packet_length) {RtpPacketReceived packet;if (!packet.Parse(rtp_packet, rtp_packet_length))return;if (packet.PayloadType() == config_.rtp.red_payload_type) {RTC_LOG(LS_WARNING) << "Discarding recovered packet with RED encapsulation";return;}packet.IdentifyExtensions(rtp_header_extensions_);packet.set_payload_type_frequency(kVideoPayloadTypeFrequency);// TODO(nisse): UlpfecReceiverImpl::ProcessReceivedFec passes both// original (decapsulated) media packets and recovered packets to// this callback. We need a way to distinguish, for setting// packet.recovered() correctly. Ideally, move RED decapsulation out// of the Ulpfec implementation.ReceivePacket(packet);
}

  5.如代码注释所写,包的recover标志位并没有被正确设置,即使是被fec恢复的包,is_recovered依然是false。

这篇关于(01)Webrtc::Fec与Nack的二三事的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/867740

相关文章

hdu 2602 and poj 3624(01背包)

01背包的模板题。 hdu2602代码: #include<stdio.h>#include<string.h>const int MaxN = 1001;int max(int a, int b){return a > b ? a : b;}int w[MaxN];int v[MaxN];int dp[MaxN];int main(){int T;int N, V;s

集中式版本控制与分布式版本控制——Git 学习笔记01

什么是版本控制 如果你用 Microsoft Word 写过东西,那你八成会有这样的经历: 想删除一段文字,又怕将来这段文字有用,怎么办呢?有一个办法,先把当前文件“另存为”一个文件,然后继续改,改到某个程度,再“另存为”一个文件。就这样改着、存着……最后你的 Word 文档变成了这样: 过了几天,你想找回被删除的文字,但是已经记不清保存在哪个文件了,只能挨个去找。真麻烦,眼睛都花了。看

雷动WEBRTC产品

http://www.rtcpower.com/html/leidongwebrtc.html ; 1.前言      WebRTC是一项在浏览器内部进行实时视频和音频通信的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得一项技术。WebRTC实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以

Apple quietly slips WebRTC audio, video into Safari's WebKit spec

转自:http://www.zdnet.com/article/apple-quietly-slips-webrtc-audio-video-into-safaris-webkit-spec/?from=timeline&isappinstalled=0 http://www.zdnet.com/article/apple-quietly-slips-webrtc-audio-video-

01 Docker概念和部署

目录 1.1 Docker 概述 1.1.1 Docker 的优势 1.1.2 镜像 1.1.3 容器 1.1.4 仓库 1.2 安装 Docker 1.2.1 配置和安装依赖环境 1.3镜像操作 1.3.1 搜索镜像 1.3.2 获取镜像 1.3.3 查看镜像 1.3.4 给镜像重命名 1.3.5 存储,载入镜像和删除镜像 1.4 Doecker容器操作 1.4

WebRTC-nack机制详解

1.NACK的含义 丢包重传(NACK)是抵抗网络错误的重要手段。NACK在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端,发送缓冲区的支持。 WebRTC中支持音频和视频的NACK重传。我们这里只分析nack机制,不分析jitterbuffer或者neteq的更多实现。 2.WebRTC

通信工程学习:什么是FEC前向纠错

FEC:前向纠错        FEC(Forward Error Correction,前向纠错)是一种增加数据通信可信度的技术,广泛应用于计算机网络、无线通信、卫星通信等多种数据传输场景中。其基本原理和特点可以归纳如下: 一、FEC前向纠错的基本原理        FEC技术通过在发送端对原始数据添加冗余信息(即纠错码),使得接收端在接收到数据后,即使部分数据在传输过程中发生错误

滚雪球学MyBatis(01):教程导读

MyBatis简介 前言 欢迎回到我们的MyBatis系列教程。在上期的内容中,我们详细介绍了MyBatis的基本概念、特点以及它与其他ORM框架(如Hibernate)的对比。我们还探讨了MyBatis在数据访问层中的优势,并解释了为什么选择MyBatis作为我们的持久化框架。在阅读了上期的内容后,相信大家对MyBatis有了初步的了解。 在本期内容中,我们将深入探讨MyBatis的基本配

python+selenium2轻量级框架设计-01框架结构

接下来会介绍一个比较简单的框架结构,先看一下分类 config文件夹里放的是配置文件 framework文件夹里面放的是公共类,常用类,还有读配置文件类、日志类、截图类、发送邮件、生成测试报告、操作读取数据库、读取Excel等,后面几篇会一一介绍 logs文件夹存放生成的日志文件 pageobject存放页面类包括元素的定位等 screenshots文件放的是生成的截图 test_

python+selenium2学习笔记POM设计模式-01模式简介

Page Object模式是Selenium中的一种测试设计模式,主要是将每一个页面设计为一个Class,其中包含页面中需要测试的元素(按钮,输入框,标题 等),这样在Selenium测试页面中可以通过调用页面类来获取页面元素,这样巧妙的避免了当页面元素id或者位置变化时,需要改测试页面代码的情况。 当页面元素id变化时,只需要更改测试页Class中页面的属性即可。 Page Object模式是