WebRTC视频JitterBuffer详解
WebRTC視頻JitterBuffer詳解
- 1 WebRTC版本
- 2 概要
- 3 JitterBuffer結構和基本流程
- 4 幀完整性 - PacketBuffer
- 4.1 包緩存
- 4.2 幀的開始和結束
- 4.3 插入RTP數據包 - PacketBuffer::InsertPacket
- 4.4 處理RTP填充包 - PacketBuffer::PaddingReceived
- 4.5 丟包檢測 - PacketBuffer::UpdateMissingPackets
- 4.6 連續包檢測 - PacketBuffer::PotentialNewFrame
- 4.7 幀完整性檢測 - PacketBuffer::FindFrames
- 4.8 總結
- 5 查找參考幀 - RtpFrameReferenceFinder
- 5.1 圖像ID - PID
- 5.2 設置參考幀 - RtpFrameReferenceFinder::ManageFramePidOrSeqNum
- 5.3 處理填充包 - RtpFrameReferenceFinder::PaddingReceived
- 5.3 更新填充包狀態 - RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding
- 5.4 處理緩存的包 - RtpFrameReferenceFinder::RetryStashedFrames
- 5.5 總結
- 6 有序輸出 - FrameBuffer
- 6.1 插入幀 - FrameBuffer::InsertFrame
- 6.2 更新參考幀信息 - FrameBuffer::UpdateFrameInfoWithIncomingFrame
- 6.3 取解碼幀 - FrameBuffer::NextFrame
- 6.4 狀態傳播 - FrameBuffer::PropagateContinuity/FrameBuffer::PropagateDecodability
- 6.6 總結
- 7 抖動與延遲
- 7.1 抖動計算
- 7.2 延遲 - VCMTiming
- 7.2.1 目標延遲 - googTargetDelayMs
- 7.2.2 當前延遲 - googCurrentDelayMs
- 7.3 平滑渲染時間 - TimestampExtrapolator
- 8 總結
1 WebRTC版本
m74。
2 概要
舊版的視頻JitterBuffer實現在VCMJitterBuffer類中,目前已經不用,新版的JitterBuffer的功能被分散到多個模塊中,主要包括:
- PacketBuffer:負責幀的完整性,保證組成幀的每個包序列號連續,并且有一個包標識幀的開始,有一個包標識幀的結束;
- RtpFrameReferenceFinder:負責給每個幀設置好參考幀,同時兼顧GOP內各幀的連續性;
- FrameBuffer:負責幀的連續性和可解碼性,這里幀的連續性是指某幀的所有參考幀都已經收到,幀的可解碼性是指某幀的所有參考幀都已經被解碼;
- VCMJitterEstimator:計算抖動(googJitterbufferMS),用于計算目標延遲(googTargetDelayMs),用于音視頻同步;
- VCMTiming:計算當前延遲(googCurrentDelayMs),用于計算渲染時間。
本文對照代碼描述上述模塊的主要工作過程。
3 JitterBuffer結構和基本流程
RtpVideoStreamReceiver類收到RTP包后,交給PacketBuffer類緩存、排序,PacketBuffer收集滿1個完整的幀后,交還給RtpVideoStreamReceiver類,RtpVideoStreamReceiver類將一個完整的幀交給RtpFrameReferenceFinder,RtpFrameReferenceFinder類緩存最近的GOP,每個完整幀落在一個GOP中會填充好該幀的參考幀,交還給RtpVideoStreamReceiver,RtpVideoStreamReceiver將填充好參考幀的完整幀交給FrameBuffer,FrameBuffer判斷某幀的所有參考幀都收到認為該幀連續,在某幀的所有參考幀都解碼后認為該幀可以解碼,從而可以交給解碼器。
可以認為JitterBuffer的這些模塊分三個層次分別做了RTP包的排序、GOP內幀的排序、GOP之間的排序:
- 包的排序:PacketBuffer;
- 幀的排序:RtpFrameReferenceFinder;
- GOP的排序:FrameBuffer。
4 幀完整性 - PacketBuffer
4.1 包緩存
PacketBuffer類有兩個類型的包緩存:
- std::vector data_buffer_,數據緩存,保存包原始數據,用于拼接整幀原始數據;
- std::vector sequence_buffer_,排序緩存,保存包連續性信息,用于緩存包序列號等信息并排序成完整的幀。
連續性信息:
struct ContinuityInfo {// 包序列號.uint16_t seq_num = 0;// 是否為幀的第一個包.bool frame_begin = false;// 是否為幀的最后一個包.bool frame_end = false;// 這個槽是否已經被使用.bool used = false;// 標識當前包之前的所有包是否都已經被插入包緩存,也就是當前包之前的所有包是否連續.bool continuous = false;// 當前包是否已經用于創建一個幀.bool frame_created = false;};4.2 幀的開始和結束
在packet_buffer.cc:348有一段注釋:
// 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這個注釋意為H264的RTP包并沒有一個可信的幀開始標識,并貼上一個7106問題鏈接,打開這個鏈接,可以看到問題在2017年的原有描述是在RTP分包方式FUA下,本該設置的幀開始標識S并沒有被正確置位,但是在2018年4月該問題被修改成可以通過first_mb_in_slice來代替FUA S位。
但是實際上即使是到目前master的最新版本代碼(13788025c81712df7e5535931a0b1d7931da6c2d )仍然還是使用FUA S位來標識FUA分包模式下一幀的第一個包,并且我測試過的多個版本(57,64,74)都沒有出現FUA S位未正常置位的情況,可能已經在17年后的版本中被修復。
在這里重點強調一幀第一個包的標識是因為該標識對判斷幀的完整性有重要作用,另外,一幀的最后一個包就是簡單根據RTP頭中的marker位來標識,只有在第一個包、最后一個包都取到并且中間的所有包都連續的情況下,才認為是一個完整的幀。
4.3 插入RTP數據包 - PacketBuffer::InsertPacket
數據緩存、排序緩存這兩個包緩存都是初始長度為size_(512)的數組,一旦緩存滿會倍增容量,直到達到最大長度max_size_(2048)。
插入包的過程就是把數據填入這兩個緩存的過程,同時會判斷是否出現丟包,如果出現丟包則等待,在沒有出現丟包的情況下,會判斷是否已經獲得了完整的幀,如果已經組裝好了若干完整的幀,則通過OnAssembledFrame回調通知RtpVideoStreamReceiver。
bool PacketBuffer::InsertPacket(VCMPacket* packet) {std::vector<std::unique_ptr<RtpFrameObject>> found_frames;{rtc::CritScope lock(&crit_);// 當前包序列號uint16_t seq_num = packet->seqNum;// 當前包在包緩存(包括數據緩存和排序緩存)中的索引size_t index = seq_num % size_;// 如果是第一個包if (!first_packet_received_) {// 保存第一個包序列號first_seq_num_ = seq_num;// 接收到了第一個包狀態置位first_packet_received_ = true;} else if (AheadOf(first_seq_num_, seq_num)) { // 如果當前包比之前記錄的第一個包first_seq_num_還老// 并且之前已經清理過第一個包序列號,說明已經至少成功解碼過一幀,RtpVideoStreamReceiver::FrameDecoded// 會調用PacketBuffer::ClearTo(seq_num),清理first_seq_num_之前的所有緩存,這個時候還來一個比first_seq_num_還// 老的包,就沒有必要再留著了。if (is_cleared_to_first_seq_num_) {delete[] packet->dataPtr;packet->dataPtr = nullptr;return false;}// 相反如果沒有被清理過,則是有必要保留成第一個包的,比如發生了亂序。first_seq_num_ = seq_num;}// 如果這個槽被占用了if (sequence_buffer_[index].used) {// 如果序列號相等,則為重復包,刪除負載并丟棄。if (data_buffer_[index].seqNum == packet->seqNum) {delete[] packet->dataPtr;packet->dataPtr = nullptr;return true;}// 如果槽被占但是輸入包和對應槽的包序列號不等,說明緩存滿了,需要擴容。while (ExpandBufferSize() && sequence_buffer_[seq_num % size_].used) {}// 重新計算輸入包索引.index = seq_num % size_;// 如果對應的槽還是被占用了,還是滿,已經不行了,致命錯誤.if (sequence_buffer_[index].used) {delete[] packet->dataPtr;packet->dataPtr = nullptr;return false;}}// 如果沒有錯誤,在index對應槽位填入當前包的信息sequence_buffer_[index].frame_begin = packet->is_first_packet_in_frame(); // 第一個包標識sequence_buffer_[index].frame_end = packet->is_last_packet_in_frame(); // 最后一個包標識sequence_buffer_[index].seq_num = packet->seqNum; // 序列號sequence_buffer_[index].continuous = false; // 之前的包是否連續,這里初始為false,在FindFrames中置位sequence_buffer_[index].frame_created = false; // 是否已經用于創建一個幀,在FindFrames中置位sequence_buffer_[index].used = true; // 槽位已經被占data_buffer_[index] = *packet; // 存入數據緩存packet->dataPtr = nullptr; // 轉移了指針的所有者// 更新丟包信息,檢查收到當前包后是否有丟包導致的空洞,也就是不連續.UpdateMissingPackets(packet->seqNum);// 更新時間戳int64_t now_ms = clock_->TimeInMilliseconds();last_received_packet_ms_ = now_ms;if (packet->frameType == kVideoFrameKey)last_received_keyframe_packet_ms_ = now_ms;// 分析排序緩存,檢查是否能夠組裝出完整的幀并返回.found_frames = FindFrames(seq_num);}// 如果有完整的幀則通過回調OnAssembledFrame上報RtpVideoStreamReceiver.for (std::unique_ptr<RtpFrameObject>& frame : found_frames)assembled_frame_callback_->OnAssembledFrame(std::move(frame));return true; }4.4 處理RTP填充包 - PacketBuffer::PaddingReceived
發送端可能在編碼器輸出碼率不足的情況下為保證發送碼率填充空包,空包不會進入排序緩存和數據緩存,但是會觸發丟包檢測和完整幀的檢測。
void PacketBuffer::PaddingReceived(uint16_t seq_num) {std::vector<std::unique_ptr<RtpFrameObject>> found_frames;{rtc::CritScope lock(&crit_);// 更新丟包信息,檢查收到當前包后是否有丟包導致的空洞,也就是不連續.UpdateMissingPackets(seq_num);// 分析排序緩存,檢查是否能夠組裝出完整的幀并返回.found_frames = FindFrames(static_cast<uint16_t>(seq_num + 1));}// 如果有完整的幀則通過回調OnAssembledFrame上報RtpVideoStreamReceiver.for (std::unique_ptr<RtpFrameObject>& frame : found_frames)assembled_frame_callback_->OnAssembledFrame(std::move(frame)); }4.5 丟包檢測 - PacketBuffer::UpdateMissingPackets
PacketBuffer維護一個丟包緩存missing_packets_,主要用于在PacketBuffer::FindFrames中判斷某個已經完整的P幀前面是否有未完整的幀,如果有,該幀可能是I幀,也可能是P幀,這里并不會立刻把這個完整的P幀向后傳遞給RtpFrameReferenceFinder,而是暫時清除狀態,等待前面的所有幀完整后才重復檢測操作,所以這里實際上也發生了幀的排序,并產生了一定的幀間依賴。
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_)) {// 丟包緩存missing_packets_最大保存1000個包,這里得到當前包1000個包以前的序列號,// 也就差不多是丟包緩存里應該保存的最老的包.uint16_t old_seq_num = seq_num - kMaxPaddingAge;// 第一個>= old_seq_num的包的位置auto erase_to = missing_packets_.lower_bound(old_seq_num);// 刪除丟包緩存里所有1000個包之前的所有包(如果有的話)missing_packets_.erase(missing_packets_.begin(), erase_to);// 如果最老的包的序列號都比當前最新包序列號新,那么更新一下當前最新包序列號if (AheadOf(old_seq_num, *newest_inserted_seq_num_))*newest_inserted_seq_num_ = old_seq_num;// 因為seq_num > newest_inserted_seq_num_,這里開始統計(newest_inserted_seq_num_, sum)之間的空洞.++*newest_inserted_seq_num_;// 從newest_inserted_seq_num_開始,每個小于當前seq_num的包都進入丟包緩存,// 直到newest_inserted_seq_num_ == seq_num,也就是最新包的序列號變成了當前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);} }4.6 連續包檢測 - PacketBuffer::PotentialNewFrame
PacketBuffer::PotentialNewFrame(uint16_t seq_num)函數用于檢測seq_num前的所有包是連續的,只有包連續,才進入完整幀的檢測,所以叫“潛在的新幀檢測”。
bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {// 通過序列號獲取緩存索引size_t index = seq_num % size_;// 上個包的索引int prev_index = index > 0 ? index - 1 : size_ - 1;// 如果當前包的槽位沒有被占用,那么該包之前沒有處理過,不連續.if (!sequence_buffer_[index].used)return false;// 如果當前包的槽位的序列號和當前包序列號不一致,不連續.if (sequence_buffer_[index].seq_num != seq_num)return false;// 如果當前包已經用于創建一個幀,不連續.if (sequence_buffer_[index].frame_created)return false;// 如果當前包的幀開始標識frame_begin為true,那么該包是幀第一個包,連續.if (sequence_buffer_[index].frame_begin)return true;// 如果上個包的槽位沒有被占用,那么上個包之前沒有處理過,不連續.if (!sequence_buffer_[prev_index].used)return false;// 如果上個包已經用于創建一個幀,不連續. if (sequence_buffer_[prev_index].frame_created)return false;// 如果上個包和當前包的序列號不連續,不連續.if (sequence_buffer_[prev_index].seq_num !=static_cast<uint16_t>(sequence_buffer_[index].seq_num - 1)) {return false;}// 如果上個包的時間戳和當前包的時間戳不相等,不連續.if (data_buffer_[prev_index].timestamp != data_buffer_[index].timestamp)return false;// 排除掉以上所有錯誤后,如果上個包連續,則可以認為當前包連續.if (sequence_buffer_[prev_index].continuous)return true;// 如果上個包不連續或者有其他錯誤,就返回不連續.return false; }從函數代碼可以看出,一個幀的第一個包當且僅當幀開始標識frame_begin == true才返回連續,而第二個包以后是否返回連續依賴于上個包是否連續,這個連續性的延展保證只要判定某個序列號連續,其之前的所有包都連續。
frame_begin在FUA分包模式下是由FUA頭的S位來設置的,所以上文說到這個標識的正確性很重要,如果S位沒有正確設置則在FUA模式下(大幀分包)會出現錯誤,所幸這個應該不會發生。
4.7 幀完整性檢測 - PacketBuffer::FindFrames
PacketBuffer::FindFrames函數會遍歷排序緩存中連續的包,檢查一幀的邊界,但是這里對VPX和H264的處理做了區分:
-
對VPX,這個函數認為包的frame_begin可信,這樣VPX的完整一幀就完全依賴于檢測到frame_begin和frame_end這兩個包;
-
對H264,這個函數認為包的frame_begin不可信,并不依賴frame_begin來判斷幀的開始,但是frame_end仍然是可信的,具體說H264的開始標識是通過從frame_end標識的一幀最后一個包向前追溯,直到找到一個時間戳不一樣的斷層,認為找到了完整的一個H264的幀。
另外這里對H264的P幀做了一些特殊處理,雖然P幀可能已經完整,但是如果該P幀前面仍然有丟包空洞,不會立刻向后傳遞,會等待直到所有空洞被填滿,因為P幀必須有參考幀才能正確解碼。
4.8 總結
- PacketBuffer::InsertPacket向包緩存插入RTP數據,并觸發幀完整性檢查;
- PacketBuffer::PaddingReceived處理空包,并觸發幀完整性檢查;
- PacketBuffer::UpdateMissingPackets,更新丟包信息,用于檢查P幀前面的空洞;
- PacketBuffer::PotentialNewFrame,判斷包的連續性,只有連續的包才檢查幀完整性;
- PacketBuffer::FindFrames,幀完整性檢查,如果得到完整幀,則通過OnAssembledFrame回調上報。
5 查找參考幀 - RtpFrameReferenceFinder
上圖描述了RtpFrameReferenceFinder的基本工作原理,顧名思義,RtpFrameReferenceFinder就是要找到每個幀的參考幀。I幀是GOP起始幀自參考,后續GOP內每個幀都要參考上一幀。
RtpFrameReferenceFinder維護最近的GOP表,收到P幀后,RtpFrameReferenceFinder找到P幀所屬的GOP,將P幀的參考幀設置為GOP內該幀的上一幀,之后傳遞給FrameBuffer。
RtpFrameReferenceFinder還保證GOP內幀的輸出連續,對H264來說,每收到一幀都判斷該幀的第一個包的序列號是否與之前GOP收到的最后一個包序列號連續,是則輸出連續幀,否則緩存等待直到連續;對VPX,只需要簡單判斷PID是否連續即可。這種連續傳遞的依賴關系會導致GOP內任一幀丟失則GOP內的剩余時間都處于卡頓狀態。
5.1 圖像ID - PID
PID(Picture ID)是每幀圖像的唯一標識,VPX定義了PID,但是H264沒有這個概念,RtpFrameReferenceFinder使用每幀的最后一個包的序列號作為H264幀的PID。
在一個GOP內,除了I幀、P幀之外,可能還有WebRTC為補償發送碼率填充的空包,也會占用一個序列號。I幀是GOP的開始,沒有連續性問題,但是要判斷當前收到的P幀是否連續則需要判斷該P幀的第一個包序列號-1是否等于該GOP當前收到的最后一個包序列號,可能是上一幀的最后一個包,也可能是一個填充包。
RtpFrameReferenceFinder定義的的GOP表結構:
| key | value |
| last_seq_num:I幀最后一個包序列號,PID | last_picture_id_gop:GOP內最新的一個幀的最后一個包的序列號, 用于設置為下一個幀的參考幀。 |
| last_picture_id_with_padding_gop:GOP內最新一個包的序列號,有可能是last_picture_id_gop,也有可能是填充包,用于檢查幀的連續性。 |
5.2 設置參考幀 - RtpFrameReferenceFinder::ManageFramePidOrSeqNum
該函數用于檢查輸入幀的連續性,并且設置其參考幀。
RtpFrameReferenceFinder::FrameDecision RtpFrameReferenceFinder::ManageFramePidOrSeqNum(RtpFrameObject* frame,int picture_id) {// 對H264,在沒有開啟generic的情況下,picture_id肯定是kNoPictureId.if (picture_id != kNoPictureId) {frame->id.picture_id = unwrapper_.Unwrap(picture_id); // 設置PIDframe->num_references = frame->frame_type() == kVideoFrameKey ? 0 : 1; // I幀自參考,P幀參考上一幀frame->references[0] = frame->id.picture_id - 1; // 參考幀是上一幀return kHandOff;}// 如果是關鍵幀,插入GOP表,key是last_seq_num,初始value是{last_seq_num, last_seq_num}if (frame->frame_type() == kVideoFrameKey) {last_seq_num_gop_.insert(std::make_pair(frame->last_seq_num(),std::make_pair(frame->last_seq_num(), frame->last_seq_num())));}// 如果GOP表空,那么就不可能找到參考幀,先緩存.if (last_seq_num_gop_.empty())return kStash;// 刪除較老的關鍵幀(PID小于last_seq_num - 100), 但是至少保留一個。auto clean_to = last_seq_num_gop_.lower_bound(frame->last_seq_num() - 100);for (auto it = last_seq_num_gop_.begin();it != clean_to && last_seq_num_gop_.size() > 1;) {it = last_seq_num_gop_.erase(it);}// 在GOP表中搜索第一個比當前幀新的關鍵幀。auto seq_num_it = last_seq_num_gop_.upper_bound(frame->last_seq_num());// 如果搜索到的關鍵幀是最老的,說明當前幀比最老的關鍵幀還老,無法設置參考幀,丟棄.if (seq_num_it == last_seq_num_gop_.begin()) {RTC_LOG(LS_WARNING) << "Generic frame with packet range ["<< frame->first_seq_num() << ", "<< frame->last_seq_num()<< "] has no GoP, dropping frame.";return kDrop;}// 如果搜索到的關鍵幀不是最老的,那么搜索到的關鍵幀的上一個關鍵幀所在的GOP里應該可以找到參考幀,// 如果當前幀是關鍵幀,seq_num_it為end(), seq_num_it--則為最后一個關鍵幀.seq_num_it--;// 保證幀的連續,不連續則先緩存.// 當前GOP的最新一個幀的最后一個包的序列號.uint16_t last_picture_id_gop = seq_num_it->second.first;// 當前GOP的最新包的序列號,可能是last_picture_id_gop, 也可能是填充包.uint16_t last_picture_id_with_padding_gop = seq_num_it->second.second;// P幀的連續性檢查.if (frame->frame_type() == kVideoFrameDelta) {// 獲得P幀第一個包的上個包的序列號.uint16_t prev_seq_num = frame->first_seq_num() - 1;// 如果P幀第一個包的上個包的序列號與當前GOP的最新包的序列號不等,說明不連續,先緩存.if (prev_seq_num != last_picture_id_with_padding_gop)return kStash;}// 現在這個幀是連續的了.RTC_DCHECK(AheadOrAt(frame->last_seq_num(), seq_num_it->first));// 獲得當前幀的最后一個包的序列號,設置為初始PID,后面還會設置一次Unwrap.frame->id.picture_id = frame->last_seq_num();// 設置幀的參考幀數,P幀才需要1個參考幀.frame->num_references = frame->frame_type() == kVideoFrameDelta;// 設置參考幀為當前GOP的最新一個幀的最后一個包的序列號,// 既然該幀是連續的,那么其參考幀自然也就是上個幀.frame->references[0] = rtp_seq_num_unwrapper_.Unwrap(last_picture_id_gop);// 如果當前幀比當前GOP的最新一個幀的最后一個包還新,則更新GOP的最新一個幀的最后一個包(first)// 以及GOP的最新包(second).if (AheadOf<uint16_t>(frame->id.picture_id, last_picture_id_gop)) {seq_num_it->second.first = frame->id.picture_id; // 更新GOP的最新一個幀的最后一個包seq_num_it->second.second = frame->id.picture_id; // 更新GOP的最新包,可能被填充包更新.}// 更新最新PID,H264無用.last_picture_id_ = frame->id.picture_id;// 更新填充包狀態.UpdateLastPictureIdWithPadding(frame->id.picture_id);// 設置當前幀的PID為Unwrap形式.frame->id.picture_id = rtp_seq_num_unwrapper_.Unwrap(frame->id.picture_id);// 該包已經設置了參考幀且連續,可以向后傳遞了.return kHandOff; }5.3 處理填充包 - RtpFrameReferenceFinder::PaddingReceived
該函數緩存填充包,并更新填充包狀態,假如該填充包剛好填補了當前GOP的序列號空洞,則有可能有緩存的P幀進入連續狀態,所以嘗試處理一次緩存的P幀。
void RtpFrameReferenceFinder::PaddingReceived(uint16_t seq_num) {rtc::CritScope lock(&crit_);// 只保留最近100個填充包.auto clean_padding_to =stashed_padding_.lower_bound(seq_num - kMaxPaddingAge);stashed_padding_.erase(stashed_padding_.begin(), clean_padding_to);// 緩存填充包.stashed_padding_.insert(seq_num);// 更新填充包狀態.UpdateLastPictureIdWithPadding(seq_num);// 嘗試處理一次緩存的P幀,有可能序列號連續了.RetryStashedFrames(); }5.3 更新填充包狀態 - RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding
該函數檢查填充包緩存中的填充包,如果在GOP內連續則更新GOP表的last_picture_id_with_padding_gop字段,保證GOP的最新包序列號為最新的填充包序列號,以保證幀的連續性檢查能夠正確運行下去。
void RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding(uint16_t seq_num) {// 獲取GOP表第一個比seq_num新的I幀.auto gop_seq_num_it = last_seq_num_gop_.upper_bound(seq_num);// 如果第一個比seq_num新的I幀在GOP表首,說明seq_num已經很老了,不處理.if (gop_seq_num_it == last_seq_num_gop_.begin())return;// 獲取seq_num所在的GOP.--gop_seq_num_it;// 計算GOP最新包的下一個連續的序列號,看看是否可以在緩存的填充包中查到。uint16_t next_seq_num_with_padding = gop_seq_num_it->second.second + 1;// 查找填充包緩存中第一個大于等于next_seq_num_with_padding的位置.auto padding_seq_num_it =stashed_padding_.lower_bound(next_seq_num_with_padding);// 如果連續的序列號都能在緩存的填充包中查到,更新GOP最新包序列號,并從填充包緩存中清除.while (padding_seq_num_it != stashed_padding_.end() &&*padding_seq_num_it == next_seq_num_with_padding) {// 更新GOP最新包的序列號為連續的填充包序列號.gop_seq_num_it->second.second = next_seq_num_with_padding;// 下個連續的填充包序列號.++next_seq_num_with_padding;// 刪除填充包緩存的當前項,指向下一個.padding_seq_num_it = stashed_padding_.erase(padding_seq_num_it);}// 在某種情況下,這個流長時間連續但是沒有獲得新的關鍵幀,當前的幀可能比上個關鍵幀// 更老(例如發生了序列號wrapping), 為防止這種情況不時的更新這個關鍵幀的PID。// 如果該GOP的關鍵幀的最后一個包的序列號(PID)早于當前包10000,更新該關鍵幀PID.if (ForwardDiff(gop_seq_num_it->first, seq_num) > 10000) {RTC_DCHECK_EQ(1ul, last_seq_num_gop_.size());// 設置新的PID為當前幀seq_num.last_seq_num_gop_[seq_num] = gop_seq_num_it->second;// 刪除舊的項.last_seq_num_gop_.erase(gop_seq_num_it);} }5.4 處理緩存的包 - RtpFrameReferenceFinder::RetryStashedFrames
有兩種情況可以嘗試處理緩存的幀,持續的輸出帶參考幀的連續的幀。
- 在輸出完一個連續的帶參考幀的幀后,幀緩存stashed_frames_中可能還可以輸出下一個連續的帶參考幀的幀;
- 收到一個亂序的填充包,導致GOP中的某個P幀連續。
5.5 總結
RtpFrameReferenceFinder緩存GOP信息,每個幀(以及填充包)進入GOP排序,如果某個幀連續,則設置其參考幀為GOP內上一幀并輸出,I幀不需要參考幀,P幀需要參考幀。
6 有序輸出 - FrameBuffer
上節的RtpFrameReferenceFinder為了設置P幀的參考幀為上一幀,保證了GOP內幀的有序,但是不保證GOP輸出的有序,這個保證是由FrameBuffer來實現。
如上圖所示,FrameBuffer按照幀的先后順序向解碼器輸出幀。FrameBuffer按順序輸出“可解碼”的幀,這里的“可解碼”意思是某幀“連續”、并且其所有參考幀都已經被解碼,這里“連續”的意思是指某個幀的所有參考幀都已經收到。I幀是自參考的,所以直接是可解碼的,但是P幀則需要等待所有參考幀,也就是上一幀被收到。
這樣,因為PacketBuffer、RtpFrameReferenceFinder這兩個類只是保證幀的完整、GOP內幀的有序,一旦當前GOP的P幀還未完整,下個GOP的I幀提前進入FrameBuffer,則會直接丟棄當前GOP的所有后續P幀。
6.1 插入幀 - FrameBuffer::InsertFrame
該函數將當前幀插入幀緩存,如果該幀的所有參考幀都已經收到,那么認為該幀是連續的,那么通過同步事件通知解碼線程取待解碼幀,同時通知參考該幀的所有幀,檢查他們的未連續參考幀數量是否已經為0,是則連續。
int64_t FrameBuffer::InsertFrame(std::unique_ptr<EncodedFrame> frame) {const VideoLayerFrameId& id = frame->id;rtc::CritScope lock(&crit_);// 上一個連續的幀的PIDint64_t last_continuous_picture_id =!last_continuous_frame_ ? -1 : last_continuous_frame_->picture_id;// 檢查參考幀是否合法,不合法則返回.if (!ValidReferences(*frame)) {RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("<< id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") has invalid frame references, dropping frame.";return last_continuous_picture_id;}// 如果幀緩存溢出了.if (frames_.size() >= kMaxFramesBuffered) {// 如果是關鍵幀.if (frame->is_keyframe()) {RTC_LOG(LS_WARNING) << "Inserting keyframe (picture_id:spatial_id) ("<< id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") but buffer is full, clearing"<< " buffer and inserting the frame.";// 清理一下,繼續從當前幀開始解碼.ClearFramesAndHistory();} else {// 如果不是關鍵幀就返回.RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("<< id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") could not be inserted due to the frame "<< "buffer being full, dropping frame.";return last_continuous_picture_id;}}// 最近解碼的幀PID(H264是幀最后一個包序列號).auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();// 最近解碼的幀時間戳.auto last_decoded_frame_timestamp =decoded_frames_history_.GetLastDecodedFrameTimestamp();// 如果當前幀的PID < 最近解碼幀的PID,有可能是亂序,也有可能是序列號wrapping.if (last_decoded_frame && id <= *last_decoded_frame) {// 雖然PID更小,但是時間戳更加新,可能是編碼器重置或者序列號wrapping,// 假如是關鍵幀的話還是可以繼續處理的.if (AheadOf(frame->Timestamp(), *last_decoded_frame_timestamp) &&frame->is_keyframe()) {// If this frame has a newer timestamp but an earlier picture id then we// assume there has been a jump in the picture id due to some encoder// reconfiguration or some other reason. Even though this is not according// to spec we can still continue to decode from this frame if it is a// keyframe.RTC_LOG(LS_WARNING)<< "A jump in picture id was detected, clearing buffer.";// 清理一下,繼續從當前幀開始解碼.ClearFramesAndHistory();last_continuous_picture_id = -1;} else {// 如果是真的亂序,而且不是關鍵幀,丟棄.RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("<< id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") inserted after frame ("<< last_decoded_frame->picture_id << ":"<< static_cast<int>(last_decoded_frame->spatial_layer)<< ") was handed off for decoding, dropping frame.";return last_continuous_picture_id;}}// 假如序列號發生了很大跳動,清理.if (!frames_.empty() && id < frames_.begin()->first &&frames_.rbegin()->first < id) {RTC_LOG(LS_WARNING)<< "A jump in picture id was detected, clearing buffer.";// 清理一下,繼續從當前幀開始解碼.ClearFramesAndHistory();last_continuous_picture_id = -1;}// 嘗試申請幀緩存的槽位.auto info = frames_.emplace(id, FrameInfo()).first;// 如果是重復幀,返回.if (info->second.frame) {RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("<< id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") already inserted, dropping frame.";return last_continuous_picture_id;}// 更新幀信息,主要是設置幀的還未連續的參考幀數量,并建立被參考幀與參考他的幀之間的參考關系,// 用于當被參考幀有效時,更新參考他的幀的參考幀數量(為0則連續)以及可解碼狀態.if (!UpdateFrameInfoWithIncomingFrame(*frame, info))return last_continuous_picture_id;// 如果不是被重傳的,可以用于計算時延.// timing_用于計算很多時延指標以及幀的預期渲染時間.if (!frame->delayed_by_retransmission())timing_->IncomingTimestamp(frame->Timestamp(), frame->ReceivedTime());// 保存幀到幀緩存info->second.frame = std::move(frame);// 如果該幀的未連續的參考幀數量為0,那么他本身已經連續,例如I幀,或者當前P幀參考的上個P幀已經收到.if (info->second.num_missing_continuous == 0) {// 設置"連續"狀態info->second.continuous = true;// 傳播"連續"狀態,也就是遍歷參考當前幀的所有幀,讓他們num_missing_continuous--PropagateContinuity(info);// 返回的最后連續幀PIDlast_continuous_picture_id = last_continuous_frame_->picture_id;// 現在肯定有"連續"幀,通知解碼線程干活.new_continuous_frame_event_.Set();}// 返回最后連續幀PIDreturn last_continuous_picture_id; }6.2 更新參考幀信息 - FrameBuffer::UpdateFrameInfoWithIncomingFrame
該函數檢查某幀的參考幀是否已經連續,初始化未連續參考幀計數器num_missing_continuous、未解碼參考幀計數器num_missing_decodable,同時反向建立被參考幀與依賴幀之間的關系,方便狀態(連續、可解碼)傳播。
bool FrameBuffer::UpdateFrameInfoWithIncomingFrame(const EncodedFrame& frame,FrameMap::iterator info) {TRACE_EVENT0("webrtc", "FrameBuffer::UpdateFrameInfoWithIncomingFrame");const VideoLayerFrameId& id = frame.id;// 最新解碼的幀.auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();RTC_DCHECK(!last_decoded_frame || *last_decoded_frame < info->first);struct Dependency {VideoLayerFrameId id; // PIDbool continuous; // 只有未連續參考幀數量為0,才為“連續”};std::vector<Dependency> not_yet_fulfilled_dependencies;// 遍歷當前幀的所有參考幀for (size_t i = 0; i < frame.num_references; ++i) {// 參考幀VideoLayerFrameId ref_key(frame.references[i], frame.id.spatial_layer);// 如果當前幀的參考幀與最新的解碼幀比相等或者更早,可能是被解過碼,也有可能是亂序。if (last_decoded_frame && ref_key <= *last_decoded_frame) {// 如果這個參考幀還未解碼(亂序),那么這個參考幀將不再有機會被解碼, 那么當前幀也無法被解碼,// 返回失敗,反之如果這個參考幀已經被解碼了,則屬于正常狀態。if (!decoded_frames_history_.WasDecoded(ref_key)) {int64_t now_ms = clock_->TimeInMilliseconds();if (last_log_non_decoded_ms_ + kLogNonDecodedIntervalMs < now_ms) {RTC_LOG(LS_WARNING)<< "Frame with (picture_id:spatial_id) (" << id.picture_id << ":"<< static_cast<int>(id.spatial_layer)<< ") depends on a non-decoded frame more previous than"<< " the last decoded frame, dropping frame.";last_log_non_decoded_ms_ = now_ms;}return false;}} else {// 如果如果當前幀的參考幀比最新的解碼幀更晚,那么該參考幀可能還未連續.auto ref_info = frames_.find(ref_key);// 檢查一下該參考幀是否已經連續.bool ref_continuous =ref_info != frames_.end() && ref_info->second.continuous;// 該參考幀填入當前幀還未滿足的依賴表.not_yet_fulfilled_dependencies.push_back({ref_key, ref_continuous});}}// 未連續參考幀計數器,初始化為當前幀還未滿足的依賴表大小.info->second.num_missing_continuous = not_yet_fulfilled_dependencies.size();// 未解碼參考幀計數器,初始化為當前幀還未滿足的依賴表大小.info->second.num_missing_decodable = not_yet_fulfilled_dependencies.size();// 遍歷當前幀還未滿足的依賴表for (const Dependency& dep : not_yet_fulfilled_dependencies) {// 如果某個參考幀已經連續if (dep.continuous)// 未連續參考幀計數器-1--info->second.num_missing_continuous;// 建立參考幀->依賴幀反向關系,用于傳播狀態.frames_[dep.id].dependent_frames.push_back(id);}return true; }6.3 取解碼幀 - FrameBuffer::NextFrame
該函數從幀緩存中獲取一個可以解碼的幀,該幀必須是連續的(所有參考幀都已經收到),并且其所有參考幀都已經被解碼。對I幀來說本身是連續的且自參考,可以直接被取走,P幀則需要依賴參考幀的連續、解碼狀態。
FrameBuffer::ReturnReason FrameBuffer::NextFrame(int64_t max_wait_time_ms,std::unique_ptr<EncodedFrame>* frame_out,bool keyframe_required) {TRACE_EVENT0("webrtc", "FrameBuffer::NextFrame");// max_wait_time_ms為最大等待時間間隔,latest_return_time_ms為最晚返回的絕對時間。int64_t latest_return_time_ms =clock_->TimeInMilliseconds() + max_wait_time_ms;int64_t wait_ms = max_wait_time_ms;int64_t now_ms = 0;do {// 當前時間now_ms = clock_->TimeInMilliseconds();{rtc::CritScope lock(&crit_);// 清除事件狀態new_continuous_frame_event_.Reset();if (stopped_)return kStopped;wait_ms = max_wait_time_ms;// 清除待解碼幀列表frames_to_decode_.clear();// 遍歷所有已經連續的幀.for (auto frame_it = frames_.begin();frame_it != frames_.end() &&frame_it->first <= last_continuous_frame_;++frame_it) {// 如果幀還未連續,或者其有參考幀還未解碼,忽略.if (!frame_it->second.continuous ||frame_it->second.num_missing_decodable > 0) {continue;}// 如果可以解碼,取到待解碼幀.EncodedFrame* frame = frame_it->second.frame.get();// 如果需要關鍵幀,但當前幀不是關鍵幀(默認keyframe_required=false), 忽略.if (keyframe_required && !frame->is_keyframe())continue;// 之前最新解碼的幀時間戳.auto last_decoded_frame_timestamp =decoded_frames_history_.GetLastDecodedFrameTimestamp();// 如果待解碼幀早于之前最新解碼的幀時間戳,亂序,不處理.if (last_decoded_frame_timestamp &&AheadOf(*last_decoded_frame_timestamp, frame->Timestamp())) {continue;}// VPX,不處理.if (frame->inter_layer_predicted) {continue;}// 收集超幀,H264只有一個完整幀,current_superframe.size()為1.std::vector<FrameMap::iterator> current_superframe;current_superframe.push_back(frame_it);// H264為true,只有一層.bool last_layer_completed =frame_it->second.frame->is_last_spatial_layer;FrameMap::iterator next_frame_it = frame_it;while (true) {// 這里面是VPX的邏輯,忽略.++next_frame_it;if (next_frame_it == frames_.end() ||next_frame_it->first.picture_id != frame->id.picture_id ||!next_frame_it->second.continuous) {break;}// Check if the next frame has some undecoded references other than// the previous frame in the same superframe.size_t num_allowed_undecoded_refs =(next_frame_it->second.frame->inter_layer_predicted) ? 1 : 0;if (next_frame_it->second.num_missing_decodable >num_allowed_undecoded_refs) {break;}// All frames in the superframe should have the same timestamp.if (frame->Timestamp() != next_frame_it->second.frame->Timestamp()) {RTC_LOG(LS_WARNING)<< "Frames in a single superframe have different"" timestamps. Skipping undecodable superframe.";break;}current_superframe.push_back(next_frame_it);last_layer_completed =next_frame_it->second.frame->is_last_spatial_layer;}// Check if the current superframe is complete.// TODO(bugs.webrtc.org/10064): consider returning all available to// decode frames even if the superframe is not complete yet.if (!last_layer_completed) {continue;}// 待解碼幀列表只有1個.frames_to_decode_ = std::move(current_superframe);// 如果未設置過渲染時間則設置渲染時間.if (frame->RenderTime() == -1) {frame->SetRenderTime(timing_->RenderTimeMs(frame->Timestamp(), now_ms));}// 檢查可以繼續等待的剩余時間.wait_ms = timing_->MaxWaitingTime(frame->RenderTime(), now_ms);// wait_ms = frame->RenderTime() - now_ms - 渲染時間 - 解碼時間// 如果wait_ms < -kMaxAllowedFrameDelayMs,說明可能解碼性能不夠,// 解碼時間過長,該幀已經來不及渲染了,忽略該幀.if (wait_ms < -kMaxAllowedFrameDelayMs)continue;// 已經獲得了待解碼幀,退出搜索.break;}} // rtc::Critscope lock(&crit_);// 更新剩余等待時間wait_ms = std::min<int64_t>(wait_ms, latest_return_time_ms - now_ms);wait_ms = std::max<int64_t>(wait_ms, 0);} while (new_continuous_frame_event_.Wait(wait_ms)); // 阻塞等待{rtc::CritScope lock(&crit_);now_ms = clock_->TimeInMilliseconds();// TODO(ilnik): remove |frames_out| use frames_to_decode_ directly.std::vector<EncodedFrame*> frames_out;// 如果獲得了可解碼幀if (!frames_to_decode_.empty()) {bool superframe_delayed_by_retransmission = false;size_t superframe_size = 0;EncodedFrame* first_frame = frames_to_decode_[0]->second.frame.get();int64_t render_time_ms = first_frame->RenderTime(); // 預期渲染時間int64_t receive_time_ms = first_frame->ReceivedTime(); // 接收時間// 檢查幀的渲染時間戳或者當前的目標延遲是否有異常,如果是則重置時間處理器,// 重新獲取幀的渲染時間.if (HasBadRenderTiming(*first_frame, now_ms)) {jitter_estimator_->Reset();timing_->Reset();render_time_ms =timing_->RenderTimeMs(first_frame->Timestamp(), now_ms);}// 遍歷所有待解碼超幀(他們應該有同樣的時間戳)for (FrameMap::iterator& frame_it : frames_to_decode_) {RTC_DCHECK(frame_it != frames_.end());EncodedFrame* frame = frame_it->second.frame.release();// 重置預期渲染時間.frame->SetRenderTime(render_time_ms);// 超幀是否經過了重傳.superframe_delayed_by_retransmission |=frame->delayed_by_retransmission();// 更新接收時間.receive_time_ms = std::max(receive_time_ms, frame->ReceivedTime());// 更新超幀總大小.superframe_size += frame->size();// 傳播可解碼性,當前幀可解碼,通知參考他的幀檢查其參考幀是否都已經被解碼,// 如果是則也可以進入可解碼狀態.PropagateDecodability(frame_it->second);// 當前可解碼幀進入已解碼幀歷史列表(實際上沒有真的被解碼,而是即將被解碼),// 早于歷史解碼幀的幀將被丟棄.decoded_frames_history_.InsertDecoded(frame_it->first,frame->Timestamp());// 刪除幀緩存開始位置到當前解碼幀位置的所有幀(因為已經沒有必要保存)frames_.erase(frames_.begin(), ++frame_it);// 輸出幀.frames_out.push_back(frame);}// 如果沒有被重傳,則可以處理延遲.if (!superframe_delayed_by_retransmission) {int64_t frame_delay;// 到達時間濾波器計算幀間延遲.if (inter_frame_delay_.CalculateDelay(first_frame->Timestamp(),&frame_delay, receive_time_ms)) {// 卡爾曼濾波器計算抖動,輸入觀測幀間延遲,輸出最優幀間延遲,也就是抖動.jitter_estimator_->UpdateEstimate(frame_delay, superframe_size);}float rtt_mult = protection_mode_ == kProtectionNackFEC ? 0.0 : 1.0;if (RttMultExperiment::RttMultEnabled()) {rtt_mult = RttMultExperiment::GetRttMultValue();}// 獲取抖動,并設置到timing_中,如果是初始狀態,當前延遲(googCurrentDelayMs)被設置成抖動.timing_->SetJitterDelay(jitter_estimator_->GetJitterEstimate(rtt_mult));// 更新當前延遲(googCurrentDelayMs),逼近googTargetDelayMs.timing_->UpdateCurrentDelay(render_time_ms, now_ms);} else {// 更新jitter_estimator_重傳的次數,會影響其獲取抖動的結果.if (RttMultExperiment::RttMultEnabled() || add_rtt_to_playout_delay_)jitter_estimator_->FrameNacked();}// 獲取詳細時間信息通知Observer.UpdateJitterDelay();UpdateTimingFrameInfo();}// 輸出待解碼幀.if (!frames_out.empty()) {if (frames_out.size() == 1) {frame_out->reset(frames_out[0]);} else {frame_out->reset(CombineAndDeleteFrames(frames_out));}return kFrameFound;}} // rtc::Critscope lock(&crit_)// 如果還有剩余時間還沒有獲得可解碼幀,可以再嘗試等一等.if (latest_return_time_ms - now_ms > 0) {// If |next_frame_it_ == frames_.end()| and there is still time left, it// means that the frame buffer was cleared as the thread in this function// was waiting to acquire |crit_| in order to return. Wait for the// remaining time and then return.return NextFrame(latest_return_time_ms - now_ms, frame_out);}return kTimeout; }6.4 狀態傳播 - FrameBuffer::PropagateContinuity/FrameBuffer::PropagateDecodability
進入FrameBuffer的幀都帶有參考幀的信息,FrameBuffer反向建立依賴表,在每個參考幀中填入依賴幀的信息,在參考幀進入連續狀態、可解碼狀態后可以直接進行通知。
連續性傳播:
void FrameBuffer::PropagateContinuity(FrameMap::iterator start) {std::queue<FrameMap::iterator> continuous_frames;// start是連續的,先入隊continuous_frames.push(start);// 廣度優先搜索傳播幀連續性.// 廣度優先搜索的基本方法:待處理數據入隊,數據出隊處理后獲得的中間數據再次入隊,// 迭代搜索直到處理完所有的數據,也就是迭代處理鄰接的節點,直到遍歷整張圖.while (!continuous_frames.empty()) {// 連續幀出隊.auto frame = continuous_frames.front();continuous_frames.pop();// 如果最新的連續幀還未設置,或者當前連續幀比之前的最新連續幀還新,那么更新最新連續幀,// 用于NextFrame中限制遍歷幀緩存的邊界.if (!last_continuous_frame_ || *last_continuous_frame_ < frame->first) {last_continuous_frame_ = frame->first;}// 遍歷當前連續幀的所有依賴幀(依賴該連續幀的幀,這些幀的參考幀就是當前連續幀)for (size_t d = 0; d < frame->second.dependent_frames.size(); ++d) {// 檢查該依賴幀是否在幀緩存中auto frame_ref = frames_.find(frame->second.dependent_frames[d]);RTC_DCHECK(frame_ref != frames_.end());// 如果該依賴幀還在幀緩存中則檢查幀連續性,否則有可能退出廣度優先搜索.if (frame_ref != frames_.end()) {// 其未連續參考幀計數器----frame_ref->second.num_missing_continuous;// 如果未連續參考幀計數器到0,說明所有參考幀都收到了.if (frame_ref->second.num_missing_continuous == 0) {// 該依賴幀也連續了.frame_ref->second.continuous = true;// 該依賴幀入隊,在下次迭代繼續搜索其依賴幀(參考他的幀)的連續性.continuous_frames.push(frame_ref);}}}} }可解碼性傳播:
void FrameBuffer::PropagateDecodability(const FrameInfo& info) {// 遍歷所有依賴幀.for (size_t d = 0; d < info.dependent_frames.size(); ++d) {// 檢查依賴幀是否還在幀緩存中.auto ref_info = frames_.find(info.dependent_frames[d]);RTC_DCHECK(ref_info != frames_.end());// TODO(philipel): Look into why we've seen this happen.if (ref_info != frames_.end()) {// 如果依賴幀還在幀緩存中,未解碼參考幀計數器--,// 一個幀只有在連續(num_missing_continuous==0),// 并且其所有參考幀已經被解碼(num_missing_decodable==0)的情況下,// 才能進入可解碼狀態(即將被解碼),該狀態在解碼線程中調用NextFrame時設置,// 所以這里不再使用廣度優先搜索傳播可解碼性,而只是遞減未解碼參考幀計數器.RTC_DCHECK_GT(ref_info->second.num_missing_decodable, 0U);--ref_info->second.num_missing_decodable;}} }6.6 總結
FrameBuffer緩存即將進入解碼器的幀,按照順序向解碼器輸出連續的、所有參考幀都已經被解碼的幀。
7 抖動與延遲
JitterBuffer包含Jitter與Buffer,上面幾節講了Buffer,主要用于緩存、排序、組幀、有序輸出,起到抗抖動的作用。但是網絡的具體抖動指標是多少,網絡的延遲是多少,需要其他的一些工具計算。
7.1 抖動計算
-
VCMInterFrameDelay:計算幀間延遲 = 兩幀的接收時間差 - 兩幀的發送時間差;
-
VCMJitterEstimator:通過VCMInterFrameDelay計算的幀間延遲計算出最優抖動值。
上圖描述了幀間延遲(抖動)觀測值的計算方法:jitter = tr_delta - ts_delta = (tr2 - tr1) - (ts2 - ts1),也就是兩幀的接收時間差 - 兩幀的發送時間差。
計算最優抖動的算法和GCC中使用到達時間濾波器(InterArrival)計算到達時間增量、使用過載估計器(OveruseEstimator)計算最優的到達間隔增量的算法基本一樣,都是利用卡爾曼濾波器,綜合幀間延遲的觀測值、預測值,獲得最優的幀間延遲(也就是網絡抖動),只是數據采樣的形式不太相同,GCC使用5ms的包簇(也可以稱為幀),這里直接使用視頻幀,這里不再詳述。
7.2 延遲 - VCMTiming
VCMTiming可以輸出接收端的以下參數,這些參數可以在使用瀏覽器拉流時在chrome://webrtc-internals頁面中看到。
| googDecodeMs | 最近一次解碼耗時. |
| googMaxDecodeMs | 最大解碼耗時,實際上是第95百分位數,也就是大于采樣集合95%的解碼延遲. |
| googRenderDelayMs | 渲染耗時,固定為10ms. |
| googJitterBufferMs | 網絡抖動,見上節. |
| googMinPlayoutDelayMs | 最小播放時延,音視頻同步器輸出的視頻幀播放應該延遲的時長. |
| googTargetDelayMs | 目標時延,googCurrentDelayMs會逼近目標延遲. |
| googCurrentDelayMs | 當前時延,用于計算視頻幀渲染時間. |
7.2.1 目標延遲 - googTargetDelayMs
int VCMTiming::TargetDelayInternal() const {return std::max(min_playout_delay_ms_,jitter_delay_ms_ + RequiredDecodeTimeMs() + render_delay_ms_); }很明顯,目標延遲基本上就是抖動+解碼時間+渲染時間,與播放延遲的最大者,也就是播放當前幀總體的期望延遲,作為當前延遲googCurrentDelayMs的參考值,并最終用于音視頻同步。
7.2.2 當前延遲 - googCurrentDelayMs
FrameBuffer每獲得一個可解碼幀會調用一次,更新當前延遲,最終用于計算渲染時間。
void VCMTiming::UpdateCurrentDelay(int64_t render_time_ms,int64_t actual_decode_time_ms) {rtc::CritScope cs(&crit_sect_);// 獲得目標延遲. uint32_t target_delay_ms = TargetDelayInternal();// render_time_ms:期望渲染時間// 期望解碼時間 = 幀期望渲染時間 - 解碼耗時 - 渲染耗時// 實際產生的延遲delayed_ms = 實際解碼時間actual_decode_time_ms - 期望解碼時間int64_t delayed_ms =actual_decode_time_ms -(render_time_ms - RequiredDecodeTimeMs() - render_delay_ms_);// 如果沒有發生延遲,退出.if (delayed_ms < 0) {return;}// 如果有延遲,上個時刻的當前延遲 + 實際產生的延遲仍然<=目標延遲if (current_delay_ms_ + delayed_ms <= target_delay_ms) {// 更新當前延遲,逼近目標延遲.current_delay_ms_ += delayed_ms;} else {// 如果上個時刻的當前延遲 + 實際產生的延遲仍然超過目標延遲,以目標延遲為上限.current_delay_ms_ = target_delay_ms;} }7.3 平滑渲染時間 - TimestampExtrapolator
FrameBuffer每獲得一個可解碼幀,都要更新其渲染時間,渲染時間通過TimestampExtrapolator類獲得。TimestampExtrapolator也是一個卡爾曼濾波器,其輸入為輸入幀的時間戳、接收時間,輸出該幀的最優期望接收時間,參考《WebRTC音視頻同步詳解》 【3.5.1.1 期望接收時間】。
視頻幀的最終渲染時間 = 最優期望接收時間 + 當前延遲。
int64_t VCMTiming::RenderTimeMsInternal(uint32_t frame_timestamp,int64_t now_ms) const {// 如果這兩個播放延遲都是0,要求立刻渲染.if (min_playout_delay_ms_ == 0 && max_playout_delay_ms_ == 0) {// Render as soon as possible.return 0;}// 使用卡爾曼濾波器估算幀平滑時間.int64_t estimated_complete_time_ms =ts_extrapolator_->ExtrapolateLocalTime(frame_timestamp);if (estimated_complete_time_ms == -1) {estimated_complete_time_ms = now_ms;}// 當前延遲限定在(min_playout_delay_ms_, max_playout_delay_ms_)范圍內int actual_delay = std::max(current_delay_ms_, min_playout_delay_ms_);actual_delay = std::min(actual_delay, max_playout_delay_ms_);// 視頻幀的最終渲染時間 = 最優期望接收時間 + 當前延遲return estimated_complete_time_ms + actual_delay; }8 總結
RTP包進入JitterBuffer后,最終輸出了完整、連續、可解碼的視頻幀,并攜帶了可用于最終播放的渲染時間。
總結
以上是生活随笔為你收集整理的WebRTC视频JitterBuffer详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机老师开学第一堂课,开学第一堂课作文
- 下一篇: 【小明有啥说啥】我不会写标题,端午随便分