日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

WebRTC视频JitterBuffer详解

發布時間:2023/12/14 编程问答 58 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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年后的版本中被修復。

bool RtpDepacketizerH264::ParseFuaNalu(RtpDepacketizer::ParsedPayload* parsed_payload,const uint8_t* payload_data) {……bool first_fragment = (payload_data[1] & kSBit) > 0;

在這里重點強調一幀第一個包的標識是因為該標識對判斷幀的完整性有重要作用,另外,一幀的最后一個包就是簡單根據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幀必須有參考幀才能正確解碼。

std::vector<std::unique_ptr<RtpFrameObject>> PacketBuffer::FindFrames(uint16_t seq_num) {std::vector<std::unique_ptr<RtpFrameObject>> found_frames;// 基本算法:遍歷所有連續包,先找到帶有frame_end標識的幀最后一個包,然后向前回溯,// 找到幀的第一個包(VPX是frame_begin, H264是時間戳不連續),組成完整一幀,// PotentialNewFrame(seq_num)檢測seq_num之前的所有包是否連續.for (size_t i = 0; i < size_ && PotentialNewFrame(seq_num); ++i) {// 當前包的緩存索引size_t index = seq_num % size_;// 如果seq_num之前所有包連續,那么seq_num自己也連續.sequence_buffer_[index].continuous = true;// 找到了幀的最后一個包.if (sequence_buffer_[index].frame_end) {size_t frame_size = 0;int max_nack_count = -1;// 幀開始序列號,從幀尾部開始.uint16_t start_seq_num = seq_num;// 幀的最小接收時間,基本是幀第一個包的接收時間.int64_t min_recv_time = data_buffer_[index].receive_time_ms;// 幀的最大接收時間,基本是最后一個包的接收時間.int64_t max_recv_time = data_buffer_[index].receive_time_ms;// 開始向前回溯,找幀的第一個包.// 幀開始的索引,從幀尾部開始.int start_index = index;// 已經測試的包數.size_t tested_packets = 0;// 當前包的時間戳.int64_t frame_timestamp = data_buffer_[start_index].timestamp;// Identify H.264 keyframes by means of SPS, PPS, and IDR.bool is_h264 = data_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;// 從幀尾部的包開始回溯.while (true) {// 測試包數++++tested_packets;// 累加幀大小frame_size += data_buffer_[start_index].sizeBytes;// 獲取最大重傳數max_nack_count =std::max(max_nack_count, data_buffer_[start_index].timesNacked);// 當前包現在被標識為已經用于創建一個幀.sequence_buffer_[start_index].frame_created = true;// 獲取最小接收時間min_recv_time =std::min(min_recv_time, data_buffer_[start_index].receive_time_ms);// 獲取最大接收時間max_recv_time =std::max(max_recv_time, data_buffer_[start_index].receive_time_ms);// 如果是VPX,并且找到了frame_begin標識的第一個包,一幀完整,回溯結束.if (!is_h264 && sequence_buffer_[start_index].frame_begin)break;// 如果是H264.if (is_h264 && !is_h264_keyframe) {// 先檢測是否關鍵幀,從數據緩存獲取H264頭.const auto* h264_header = absl::get_if<RTPVideoHeaderH264>(&data_buffer_[start_index].video_header.video_type_header);if (!h264_header || h264_header->nalus_length >= kMaxNalusPerPacket)return found_frames;// 遍歷所有NALU,注意WebRTC所有IDR幀前面都會帶SPS、PPS.for (size_t j = 0; j < h264_header->nalus_length; ++j) {if (h264_header->nalus[j].type == H264::NaluType::kSps) {has_h264_sps = true; // 找到了SPS} else if (h264_header->nalus[j].type == H264::NaluType::kPps) {has_h264_pps = true; // 找到了PPS} else if (h264_header->nalus[j].type == H264::NaluType::kIdr) {has_h264_idr = true; // 找到了IDR}}// 默認sps_pps_idr_is_h264_keyframe_為false,也就是說只需要有IDR幀就認為是關鍵幀,// 而不需要等待SPS、PPS完整.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;}}// 如果檢測包數已經達到緩存容量,中止.if (tested_packets == size_)break;// 搜索指針向前移動一個包.start_index = start_index > 0 ? start_index - 1 : 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=7106// 這里保留了注釋,可以看看H264不使用frame_begin的原因,實際上應該也可以.if (is_h264 && // 如果是H264(!sequence_buffer_[start_index].used || // 如果該槽位未被占用,發現斷層.data_buffer_[start_index].timestamp != frame_timestamp)) { // 如果時間戳不一致,發現斷層.break; // 結束回溯. }// 如果仍然在一幀內,開始包序列號--.--start_seq_num;}// 到這里幀的開始和結束位置已經搜索完畢,可以開始組幀.// 但是對H264 P幀,需要做另外的特殊處理,雖然P幀可能已經完整,// 但是如果該P幀前面仍然有丟包空洞,不會立刻向后傳遞,會等待直到所有空洞被填滿,// 因為P幀必須有參考幀才能正確解碼。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");}// 設置數據緩存中的關鍵幀標識.const size_t first_packet_index = start_seq_num % size_;RTC_CHECK_LT(first_packet_index, size_);if (is_h264_keyframe) {data_buffer_[first_packet_index].frameType = kVideoFrameKey;} else {data_buffer_[first_packet_index].frameType = kVideoFrameDelta;}// missing_packets_.upper_bound(start_seq_num) != missing_packets_.begin()// 這個條件是說在丟包的列表里搜索>start_seq_num(幀開始序列號)的第一個位置,// 發現其不等于丟包列表的開頭, 有些丟的包序列號小于start_seq_num, 也就是說P幀前面有丟包空洞, // 舉例1:// missing_packets_ = { 3, 4, 6}, start_seq_num = 5, missing_packets_.upper_bound(start_seq_num)==6// 作為一幀開始位置的序列號5,前面還有3、4這兩個包還未收到,那么對P幀來說,雖然完整,但是向后傳遞也可能是沒有意義的, // 所以這里又清除了frame_created狀態,先繼續緩存,等待丟包的空洞填滿.// 舉例2:// missing_packets_ = { 10, 16, 17}, start_seq_num = 3, missing_packets_.upper_bound(start_seq_num)==10// 作為一幀開始位置的序列號3,前面并沒有丟包,并且幀完整,那么可以向后傳遞.if (!is_h264_keyframe && missing_packets_.upper_bound(start_seq_num) !=missing_packets_.begin()) {uint16_t stop_index = (index + 1) % size_;while (start_index != stop_index) {sequence_buffer_[start_index].frame_created = false;start_index = (start_index + 1) % size_;}// 返回找到的所有完整幀.return found_frames;}}// 馬上要組幀了,清除丟包列表中到幀開始位置之前的丟包.// 對H264 P幀來說,如果P幀前面有空洞不會運行到這里,在上面已經解釋.// 對I幀來說,可以丟棄前面的丟包信息(?).missing_packets_.erase(missing_packets_.begin(),missing_packets_.upper_bound(seq_num));// 組一個幀.found_frames.emplace_back(new RtpFrameObject(this, start_seq_num, seq_num, frame_size,max_nack_count, min_recv_time, max_recv_time));} // if (sequence_buffer_[index].frame_end)// 向后擴大搜索的范圍,假設丟包、亂序,當前包的seq_num剛好填補了之前的一個空洞,// 該包并不能檢測出一個完整幀,需要這里向后移動指針到frame_end再進行回溯,直到檢測出完整幀,// 這里會繼續檢測之前緩存的因為前面有空洞而沒有向后傳遞的P幀。++seq_num;}// 返回找到的所有完整幀.return found_frames; }

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表結構:

keyvalue
last_seq_num:I幀最后一個包序列號,PIDlast_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幀連續。
void RtpFrameReferenceFinder::RetryStashedFrames() {bool complete_frame = false;do {complete_frame = false;// 遍歷緩存的幀for (auto frame_it = stashed_frames_.begin();frame_it != stashed_frames_.end();) {// 調用ManageFramePidOrSeqNum來處理一個緩存幀,檢查是否可以輸出帶參考幀的連續的幀.FrameDecision decision = ManageFrameInternal(frame_it->get());// 檢查處理結果switch (decision) {case kStash: // 仍然不連續,或者沒有參考幀.++frame_it; // 檢查下一個緩存幀.break;case kHandOff: // 找到了一個帶參考幀的連續的幀.complete_frame = true;// 通過OnCompleteFrame回調輸出.frame_callback_->OnCompleteFrame(std::move(*frame_it));RTC_FALLTHROUGH();case kDrop: // 無論kHandOff、kDrop都可以從緩存中刪除了.frame_it = stashed_frames_.erase(frame_it); // 刪除并檢查下一個緩存幀.}}} while (complete_frame); // 如果能持續找到帶參考幀的連續的幀則繼續. }

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详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

日韩精品久久久久 | 国产精品成人一区二区 | 久久人网| 欧美性色19p | 国产91精品看黄网站在线观看动漫 | 久久人人爽人人爽人人片av免费 | 日日夜夜操操操操 | 亚洲欧洲一区二区在线观看 | 在线视频成人 | 国产h在线观看 | 久久综合成人 | 国产一区二区三区免费视频 | 韩日三级av | 91亚洲综合 | 人人干狠狠操 | 91入口在线观看 | 亚洲涩涩网站 | www.色综合.com| 天天天天天干 | 欧美国产91 | 国产精品欧美日韩在线观看 | 久久激情视频 久久 | 在线观看免费成人 | 在线观看视频黄 | 亚洲精品国产视频 | 在线黄色免费av | 久久久久国产一区二区三区 | 婷婷色av | 国产成人av在线影院 | 亚洲综合爱 | 久久精品老司机 | 人人添人人澡人人澡人人人爽 | 久久久久久麻豆 | 狠狠狠干 | 中文字幕国语官网在线视频 | 国产精品大全 | 日本精品一区二区三区在线播放视频 | 亚洲一区网站 | 欧美极品一区二区三区 | 亚洲精品综合欧美二区变态 | 国产成人精品一区二区三区福利 | www.日日操.com| 久久视频6| 亚洲精品国产精品国自产在线 | 十八岁以下禁止观看的1000个网站 | 国产h片在线观看 | 91麻豆精品国产 | 在线天堂v | 亚州av一区 | 国产久视频| 精品亚洲成a人在线观看 | 欧美日韩国产一区二区三区在线观看 | 免费欧美高清视频 | av资源中文字幕 | 人人爽人人射 | 免费在线国产精品 | www.夜夜操| 亚洲男模gay裸体gay | 九九热只有精品 | 国产精品久久久久aaaa九色 | 91在线免费看片 | 粉嫩av一区二区三区四区五区 | 久久免费视屏 | 日本丶国产丶欧美色综合 | 就色干综合| 午夜电影久久 | 激情av在线播放 | 国产精品一区一区三区 | 狠狠干成人综合网 | 日韩av电影免费在线观看 | 久久久av免费 | 青青草国产精品视频 | 一级一级一片免费 | 精品亚洲免费 | 亚洲性少妇性猛交wwww乱大交 | 热久久免费视频精品 | 国产午夜不卡 | av在线色| 国产国产人免费人成免费视频 | 欧美性生活小视频 | 波多野结衣视频在线 | 欧美三级高清 | 中文字幕国产 | 五月天狠狠操 | 色网站国产精品 | 精品国内自产拍在线观看视频 | 亚洲精品国偷自产在线99热 | 91九色在线视频观看 | 亚洲 综合 激情 | 亚洲五月六月 | 最近中文字幕第一页 | 99热免费在线 | 国精产品满18岁在线 | .精品久久久麻豆国产精品 亚洲va欧美 | 91丨九色丨蝌蚪丨对白 | 91精品一区二区在线观看 | av一级片网站 | 国产精品com | 四虎国产永久在线精品 | 四虎视频 | 波多野结衣在线播放视频 | 永久免费毛片 | 国产精品久久久久av福利动漫 | 337p日本欧洲亚洲大胆裸体艺术 | 日韩电影在线看 | 在线亚洲精品 | 香蕉视频国产在线观看 | 日韩免费中文 | 成人在线中文字幕 | 久久久精品综合 | 精品国产一区二 | 综合国产在线观看 | 国内精品免费久久影院 | 欧美一级特黄高清视频 | 亚洲女同videos | 亚洲一区二区视频在线播放 | 国产精品视频永久免费播放 | 亚洲精品乱码久久久久久蜜桃动漫 | 久久夜色精品亚洲噜噜国4 午夜视频在线观看欧美 | 久久久久成人精品免费播放动漫 | 91成熟丰满女人少妇 | 69精品人人人人 | 亚洲区另类春色综合小说校园片 | 天天色天天草天天射 | 天天干天天干天天 | 色婷婷狠 | 黄色av在| 人人爽人人爽人人片av | 欧美成人中文字幕 | 精品亚洲视频在线观看 | 久久精品国产亚洲精品 | 国产精品视频免费 | 国产精品美女久久久久久免费 | 久久久高清 | 美女av免费 | 亚洲黄色成人网 | 国产高清视频免费观看 | 狠狠操天天操 | 88av色| 国产一级片毛片 | 天天天天综合 | 91网页版免费观看 | 国产精品18久久久久久不卡孕妇 | 一本一道久久a久久精品 | 免费看成人 | 超级碰碰免费视频 | 1024在线看片| 日韩久久午夜一级啪啪 | 久久在线一区 | 91探花国产综合在线精品 | 国产欧美精品在线观看 | 欧美91av| 国产一区二区久久久久 | 欧美了一区在线观看 | 国产一级片一区二区三区 | 亚洲一二三久久 | 在线免费av电影 | 久久国产福利 | 日本中文字幕影院 | 免费看一级特黄a大片 | 就要干b | 99在线精品视频观看 | 亚洲国产欧美一区二区三区丁香婷 | 亚洲免费小视频 | 午夜av在线免费 | 四虎精品成人免费网站 | 亚洲va天堂va欧美ⅴa在线 | 在线精品国产 | 欧美极品在线播放 | 久久精品一区二区三区四区 | 日本少妇高清做爰视频 | 午夜久久久久久久久久影院 | 极品国产91在线网站 | 97精品国产一二三产区 | 玖玖在线看| 黄色的网站在线 | 亚洲免费视频在线观看 | 黄色片网站av | 欧美一二三区在线观看 | 在线观看蜜桃视频 | 欧美日韩精品免费观看视频 | 91精品国产自产老师啪 | 午夜狠狠操| 国产精品久久久久久久久久久久冷 | 91丨九色丨91啦蝌蚪老版 | 天天超碰 | 日韩va欧美va亚洲va久久 | 国产在线观看a | 日韩com | 国产香蕉视频在线播放 | 中文字幕在线观 | 亚洲综合小说电影qvod | 免费看污污视频的网站 | 国产尤物在线视频 | 在线va网站 | 99在线视频精品 | 国产五月婷婷 | 97碰在线视频 | 看片一区二区三区 | 99久久精品国产网站 | 中文一二区 | 91精品视屏 | av中文在线观看 | 在线a人v观看视频 | 国产精品麻豆一区二区三区 | 亚洲好视频 | 一区二区精品在线观看 | 91免费看黄 | 日本动漫做毛片一区二区 | 最新黄色av网址 | 国产精品原创视频 | 国产精品资源网 | 午夜成人免费电影 | 国内精品久久久久久久久久久 | 午夜12点 | 天天操天天射天天爱 | 色综合天天色综合 | 69欧美视频| 日韩在线视频线视频免费网站 | 中文字幕精品在线 | av专区在线 | 午夜色大片在线观看 | 久久精品91久久久久久再现 | 激情婷婷六月 | 在线超碰av | 黄色网址a| 免费看十八岁美女 | 天天射天天射 | 欧美天堂视频在线 | 天天爽夜夜爽人人爽曰av | 久草五月 | 亚洲国产精品电影在线观看 | 国产成人精品一区一区一区 | 久草视频中文 | 国产精品欧美一区二区三区不卡 | 久久成人国产精品一区二区 | 久久综合国产伦精品免费 | 九九视频免费观看视频精品 | 91在线视频观看 | 欧美夫妻生活视频 | 在线观看亚洲精品视频 | 亚洲欧美精品一区 | 一区精品久久 | 国产精品久久久久影院 | 丁香婷婷综合激情五月色 | 在线看小早川怜子av | www.狠狠色.com | 成年人国产精品 | 欧美日韩高清一区 | 久视频在线 | 免费在线观看午夜视频 | 欧美日韩国产综合网 | 婷婷久久婷婷 | 欧美人交a欧美精品 | www久久九 | 欧美久久久久久久久久 | 国产不卡在线视频 | 啪啪资源 | 成人a免费看| 超级av在线| 天天插天天干天天操 | 久久国产精品偷 | 亚洲精品在线观 | 一区二区三区日韩在线观看 | 狠狠色伊人亚洲综合网站野外 | 国产视频97 | 久草视频网 | 男女全黄一级一级高潮免费看 | 视频国产区| 丁香六月网 | 天天在线操 | 大型av综合网站 | 久久激情五月激情 | 91久久国产自产拍夜夜嗨 | 国产成人精品一区二区三区免费 | 人人爽人人av | 日日夜夜狠狠 | 有码视频在线观看 | 亚洲欧美视频在线 | 在线视频精品播放 | 国内精品在线观看视频 | 国产精品videossex国产高清 | 成人一区二区三区中文字幕 | 伊人天堂av | 久久久久久久久久久高潮一区二区 | 首页国产精品 | 在线观看国产区 | 在线成人一区 | 日韩精品中文字幕在线播放 | 欧美精品999| 在线亚洲播放 | 国内精品视频久久 | 亚洲第一区在线播放 | 黄色av网站在线观看免费 | 久久99婷婷| 久章操| 在线中文日韩 | 操操操影院 | 日韩免费视频线观看 | 综合久久婷婷 | 午夜久久视频 | 国产只有精品 | 久草在线在线精品观看 | 热久久精品在线 | 欧美精品一区二区免费 | 久久影视精品 | 日本久久久久久 | 最新av中文字幕 | 国内久久看 | 国产高清免费在线观看 | 亚洲欧美日韩中文在线 | 高清不卡毛片 | 亚洲理论影院 | 欧美日韩国产在线 | 狂野欧美激情性xxxx欧美 | 欧美视频网址 | 狠狠狠狠狠狠狠干 | 天天色婷婷 | bbbb操bbbb| 五月婷婷丁香激情 | 精品一区91| 久免费视频 | 中文字幕免费不卡视频 | 久久精品视频在线免费观看 | 视频国产在线 | 亚洲国产精品va在线看黑人动漫 | 在线观看视频97 | 一区二区影院 | 欧美日韩国产精品一区 | 米奇影视7777 | 狂野欧美激情性xxxx欧美 | 亚洲精品久久久久中文字幕二区 | 久久8精品| 日本韩国精品在线 | 黄色1级毛片 | 在线观看亚洲免费视频 | 久久免费电影 | 久保带人 | .国产精品成人自产拍在线观看6 | 在线播放 日韩专区 | 天天草天天摸 | 久草男人天堂 | 九九热视频在线 | 五月天久久 | 日韩成人精品 | 日韩高清 一区 | 欧美婷婷色 | 黄色av高清 | 91 在线视频 | av成人在线播放 | 99r在线 | 在线日韩亚洲 | 99综合久久 | 福利视频精品 | 国产精品乱码久久 | 99久久免费看 | 天天天天天干 | 婷婷色中文 | 中文字幕人成不卡一区 | 日韩在线电影一区二区 | 午夜丁香视频在线观看 | 九九九九色 | 91亚洲夫妻 | 亚洲欧美视频网站 | 久久大视频 | 亚洲理论视频 | 美女视频黄在线观看 | 久久视频在线 | 日韩精品不卡在线观看 | 98福利在线 | 最近中文国产在线视频 | 欧美成年人在线视频 | 中文日韩在线视频 | 久久高清av | 久久久麻豆视频 | 免费在线观看中文字幕 | www.五月激情.com | 日韩av在线免费播放 | 久久久久久综合网天天 | 欧美日韩在线视频一区 | 久久久久久久久久久成人 | 激情五月开心 | 日韩亚洲在线观看 | 国产精品久久久久久妇 | 国产成人精品一区二区三区免费 | 免费看的黄色小视频 | 91资源在线视频 | 亚洲女在线 | 色婷av | 久久久久久久久久免费视频 | 国产视频一区二区三区在线 | 亚洲精品欧美专区 | 久久久久久久影院 | 夜夜视频欧洲 | 精品不卡av | 黄色官网在线观看 | 精品国产精品国产偷麻豆 | 久久国产精品免费看 | 天天色播| 成人在线黄色 | 日日夜夜网 | 国产免费视频在线 | 中文字幕日韩精品有码视频 | 美女黄网久久 | 国产亚洲精品久久久久5区 成人h电影在线观看 | 国产麻豆果冻传媒在线观看 | 国产精品网站 | 国产在线久草 | 最新影院 | 日韩欧三级 | 五月天六月婷 | 午夜精品电影 | 视频在线播放国产 | 香蕉网在线观看 | 国产女教师精品久久av | 精品视频9999 | 欧美 日韩精品 | wwwav视频| 97视频免费在线观看 | 国产精品久久中文字幕 | 91最新地址永久入口 | 国产日韩精品视频 | 日韩高清一二三区 | av在线播放网址 | 国产高清免费 | 激情视频一区二区三区 | 精品久久久久久久久久久久久久久久 | 97在线观看免费高清完整版在线观看 | 婷婷丁香激情综合 | 亚洲精品资源在线 | 999久久久久久久久久久 | 免费精品视频在线观看 | 亚洲精品成人网 | www视频在线免费观看 | 日韩大片在线看 | 久久手机免费视频 | 中文字幕色站 | 亚洲天堂网在线观看视频 | 97超碰国产精品女人人人爽 | 天天操天天操天天操天天操 | 久久伦理电影网 | av成人在线网站 | 国产人成在线观看 | 黄色片免费看 | 久久视频在线观看中文字幕 | 欧美精品一区二区三区四区在线 | 在线观看不卡视频 | 国产一区二区三精品久久久无广告 | 国产欧美日韩一区 | 国产中文在线播放 | 国产精品乱码久久久久久1区2区 | 色婷婷av一区 | 亚洲精品理论 | 中文字幕免费成人 | 92精品国产成人观看免费 | 成人免费中文字幕 | 综合伊人久久 | 国产美女视频免费观看的网站 | 天天插夜夜操 | 97精品欧美91久久久久久 | 日韩午夜剧场 | 狠狠色丁香婷婷综合久小说久 | 69国产精品视频免费观看 | 日韩视频图片 | 一区二区三区免费在线观看 | av电影在线观看完整版一区二区 | 国际精品久久久久 | 国产二级视频 | 九月婷婷色 | 九九亚洲视频 | 一区二区三区四区精品视频 | 天天干天天操天天入 | 国产91对白在线 | 精品国产一区二区三区av性色 | 日日日爽爽爽 | 国内精品久久久久影院优 | 一级做a视频 | 狠狠干夜夜爽 | 精品国产乱子伦一区二区 | 天天干天天怕 | 有码视频在线观看 | 亚洲欧美精品一区 | 成年人在线观看免费视频 | 狠狠躁天天躁综合网 | 夜夜躁日日躁狠狠躁 | 六月天综合网 | 国内免费久久久久久久久久久 | 麻豆国产精品va在线观看不卡 | 久久国产免费看 | 九九在线精品视频 | 97影视 | avlulu久久精品 | 成人激情开心网 | 日韩午夜大片 | 久久婷婷综合激情 | 国产中文字幕精品 | 人人超碰人人 | 69精品在线 | 久久 一区| 精品在线观看一区二区三区 | 99免费观看视频 | 九九九视频精品 | 国产精品久久久99 | 色香com. | 亚洲精品视频在线免费 | 久久久久激情 | 日日夜夜人人天天 | 国产精品每日更新 | 18岁免费看片 | 狠狠狠色丁香婷婷综合激情 | 国产96视频 | 91视频免费网站 | 密桃av在线 | 99精品偷拍视频一区二区三区 | 亚洲精品在线二区 | 狠狠狠色丁香综合久久天下网 | 日本天天色 | 91精品久 | 国产精品久久视频 | 中日韩男男gay无套 日韩精品一区二区三区高清免费 | av电影在线播放 | 久草在线费播放视频 | 亚洲精品在线视频网站 | 日韩手机在线 | 亚洲黄电影| 久久久免费播放 | 91丨九色丨国产丨porny精品 | 日本视频久久久 | 欧美伦理一区 | 毛片网站在线 | 久久黄色免费 | 国产成人精品国内自产拍免费看 | 亚洲黄色三级 | 久草久草久草久草 | 国产中文字幕在线视频 | 中文字幕在线观看你懂的 | 免费观看特级毛片 | 五月天婷婷丁香花 | 久久99精品国产99久久6尤 | 五月婷婷综合网 | 日本中文在线观看 | 91成人在线观看喷潮 | 国产一区免费视频 | 91麻豆精品国产91久久久久久久久 | 久久免费视频在线观看6 | 国产欧美最新羞羞视频在线观看 | 午夜在线看片 | 9999国产| 中文字幕国产精品一区二区 | 丁香综合五月 | 色999精品| 在线色资源 | 久久一区二| 99精品欧美一区二区蜜桃免费 | 中文乱码视频在线观看 | www.亚洲精品在线 | 最近日韩免费视频 | 手机在线免费av | 婷婷综合视频 | 欧美久久久久久久久中文字幕 | 亚洲有 在线 | av一区二区三区在线 | 成人国产电影在线观看 | 911精品美国片911久久久 | 色婷婷亚洲| 亚洲成a人片77777潘金莲 | 国产精品久久久久久999 | 亚洲全部视频 | 五月开心综合 | 91亚洲精品乱码久久久久久蜜桃 | 在线观看日韩精品 | 又黄又爽的免费高潮视频 | 国产精品美女999 | 亚州av网站大全 | 日韩视频一区二区在线 | 亚洲香蕉视频 | 免费在线国产视频 | 久久国产午夜精品理论片最新版本 | 久久精品欧美一区 | 国内精品久久久精品电影院 | 黄色免费av | 欧美在线视频二区 | 精品亚洲欧美无人区乱码 | 日韩av电影手机在线观看 | 国产色妞影院wwwxxx | 韩国三级av在线 | 狠狠色综合欧美激情 | 国产精品入口麻豆 | 色综合在| 欧美日韩一区二区在线观看 | 国产精品亚 | av观看免费在线 | 蜜臀91丨九色丨蝌蚪老版 | 久久99亚洲网美利坚合众国 | a√资源在线 | 丝袜美腿在线视频 | 特黄特色特刺激视频免费播放 | 欧美精品乱码久久久久 | www.狠狠插.com| 天天躁日日躁狠狠躁av中文 | 亚洲最新av在线网站 | 久久久久久久久久久久国产精品 | 一区二区三区四区五区在线 | 色搞搞| 精品久久久久久久久亚洲 | 国产美腿白丝袜足在线av | 91精品国产乱码久久桃 | 日韩91在线| 丁香六月五月婷婷 | 天天干天天拍 | 国产亚洲婷婷免费 | 亚洲一二视频 | 久久综合狠狠综合久久狠狠色综合 | 伊人伊成久久人综合网站 | 亚洲精品av中文字幕在线在线 | 夜夜爽www | 在线视频你懂得 | 视频一区在线免费观看 | 亚洲精选在线 | av免费看电影 | 国产中文字幕在线播放 | 天堂av在线网 | 国产精品成人自产拍在线观看 | 亚洲成人黄色网址 | 久草在线手机视频 | 在线观看av网站 | 一性一交视频 | 久久理论电影 | 国产黄色在线观看 | 亚洲精品66 | 视频99爱 | 在线影视 一区 二区 三区 | 国产成在线观看免费视频 | 日韩在线高清免费视频 | 丰满少妇在线观看网站 | 欧美精品做受xxx性少妇 | 久久久久国产成人精品亚洲午夜 | 国产一区二区三区久久久 | 日b视频在线观看网址 | 久久久久中文字幕 | 免费三级黄色 | 久久理论片 | 成人动漫一区二区 | 久久久官网 | av在线影片 | 亚洲综合色视频 | 国产资源av | 五月婷婷视频在线 | 狠狠综合网 | 精品欧美一区二区在线观看 | 婷婷丁香色综合狠狠色 | 国产不卡在线观看 | 国产一区二区中文字幕 | 亚洲视频每日更新 | 最近能播放的中文字幕 | 色视频在线看 | 九九有精品 | 日韩大片在线看 | 欧美成天堂网地址 | 国产五月 | 天天操天天吃 | 4hu视频| 午夜精品久久久99热福利 | 日韩av电影手机在线观看 | 久久午夜网 | 一个色综合网站 | 成年人免费看的视频 | 久久久久久看片 | 天堂av在线网址 | 欧美成人h版在线观看 | 精品一区二区三区久久 | 久久艹艹| 在线观看国产日韩欧美 | 人人超碰人人 | 亚洲专区在线播放 | 日本中文字幕一二区观 | 免费a视频在线观看 | 国产精品一区二区三区在线 | 草久视频在线 | 欧美在线视频第一页 | 麻豆综合网 | 丁五月婷婷 | 97视频久久久 | 国产精品一区二区av麻豆 | 麻豆一精品传二传媒短视频 | 在线观看成人一级片 | 久久国产精品久久久 | 免费三级黄色 | 久久精品站 | 婷婷色在线观看 | 久草国产在线 | 久久久久久蜜桃一区二区 | 免费视频 你懂的 | 热久久视久久精品18亚洲精品 | 国产亚洲精品久久久久秋 | 高清av中文字幕 | 精品亚洲免费 | 欧美激情综合色综合啪啪五月 | 91在线91| 亚洲人人精品 | 亚洲国产精品久久久久久 | 狠狠躁天天躁综合网 | 日韩视频a | 欧美 亚洲 另类 激情 另类 | 亚洲精品视频在线观看免费 | 日韩精品中文字幕在线 | 黄色免费视频在线观看 | 日韩乱理| 夜夜操夜夜干 | 成人国产一区二区 | 国产精品久久久久久久久久久久午夜片 | 88av色| 免费在线观看av网址 | 国产视频午夜 | 久久久久亚洲精品中文字幕 | 免费人人干 | 国产免费观看视频 | 国产精品嫩草影院99网站 | 首页国产精品 | 一区二区三区 亚洲 | av视屏在线 | 日韩电影中文 | 免费福利在线视频 | 日韩av在线影视 | 97精品国产97久久久久久春色 | 深夜免费福利网站 | 一本一本久久a久久精品综合 | 日韩小视频 | 黄色片免费看 | www.色午夜.com | 久久精品电影院 | 特级a老妇做爰全过程 | 午夜视频99 | 国产精品6999成人免费视频 | 日日操日日操 | 久久久免费视频播放 | 99超碰在线播放 | 97夜夜澡人人双人人人喊 | 96av麻豆蜜桃一区二区 | 激情www | 香蕉视频在线播放 | 免费看的视频 | 欧美日韩性视频 | 九九综合九九 | 亚洲人久久久 | 水蜜桃亚洲一二三四在线 | 一本一道久久a久久精品 | 色婷婷激情五月 | 一区二区三区日韩在线 | 中文字幕色站 | 激情动态 | 在线观看国产www | 国产精品日韩久久久久 | 久av在线| 免费中文字幕视频 | 日韩免费高清在线观看 | 国产精品网红福利 | 一级成人在线 | 精品久久久久久久久久久院品网 | 久久精品亚洲一区二区三区观看模式 | 麻豆传媒视频观看 | 国产精品一区二区麻豆 | 免费裸体视频网 | 欧美精品久久久久久 | 久色免费视频 | 国产一卡久久电影永久 | 国产精品一区二区在线观看免费 | 在线免费黄色 | 日韩高清av在线 | 成人影片在线播放 | 99久久综合国产精品二区 | 久草在线资源观看 | 久久男人影院 | 日韩精品久久久免费观看夜色 | 久久国产精品偷 | 成人小视频免费在线观看 | av免费网站在线观看 | 香蕉国产91| 91网免费看 | 国产精品 中文在线 | 热re99久久精品国产66热 | 99热国产在线中文 | 亚洲精品va| 久久伊人热 | 综合激情av | 国产白浆视频 | www.亚洲黄色| 国产 一区二区三区 在线 | 国产欧美三级 | 99久久国产免费免费 | 国产欧美三级 | 激情婷婷六月 | 五月婷久久 | 日韩高清免费在线观看 | 午夜国产福利视频 | 久久精品看 | 六月色| 欧美一级日韩三级 | 国产精品日韩在线观看 | 99国产一区二区三精品乱码 | 在线国产中文字幕 | 午夜久久成人 | 中文字幕一区二区在线观看 | 国产精品永久在线 | 国产尤物在线 | 成人a视频在线观看 | 88av网站 | 国产高清在线视频 | 久久99精品一区二区三区三区 | 久久精品一二三区 | 国产香蕉97碰碰碰视频在线观看 | 中文在线免费观看 | 麻豆传媒电影在线观看 | 三级动态视频在线观看 | 久艹在线免费观看 | 色五丁香 | 久草99 | 欧美色噜噜噜 | 尤物97国产精品久久精品国产 | 91亚洲精品乱码久久久久久蜜桃 | 一本一本久久a久久精品综合妖精 | 欧美日韩免费视频 | 五月天亚洲综合 | 久青草电影 | 欧美一级免费黄色片 | 麻豆免费精品视频 | 三级免费黄色 | 毛片网在线| 四虎4hu永久免费 | 91在线播放国产 | 久久精品99国产精品酒店日本 | 成人在线免费看 | 中文字幕一二三区 | 天天综合网天天综合色 | 又黄又刺激视频 | 国产精品麻 | 91精品国自产拍天天拍 | 欧美成天堂网地址 | 国产高清av在线播放 | 黄色99视频 | 天天伊人网 | 国产精品第52页 | 国产日本亚洲高清 | 天天色天天射综合网 | 日韩av中文 | 免费在线激情电影 | 特级a毛片 | 午夜精品久久一牛影视 | 在线免费观看黄网站 | 国产中文a | 日韩精品视频在线观看网址 | 超级碰99| 深爱五月网 | 91麻豆精品国产91久久久久久久久 | 黄色中文字幕在线 | 久久久免费少妇 | 色a综合 | 麻豆系列在线观看 | 日韩欧美在线观看一区 | 久久国产剧场电影 | 国产精品美女久久久久久久久久久 | 久久久久久久久久国产精品 | 国产精品久久久免费 | 色噜噜日韩精品一区二区三区视频 | 日韩免费电影 | 超碰在线网 | 亚洲黄色片一级 | 日韩午夜一级片 | 国产欧美在线一区二区三区 | 精品福利网 | 99在线视频观看 | 日韩综合在线观看 | 日韩欧美视频在线 | 国产亚洲视频在线观看 | 亚洲精品乱码久久久久久蜜桃91 | 久草精品视频在线看网站免费 | 欧美另类美少妇69xxxx | 99一级片 | 国产精品一区二区三区久久 | 91av在线免费看 | 日韩av成人免费看 | 999久久a精品合区久久久 | 婷婷六月天综合 | 亚洲国产精品久久 | 91在线91拍拍在线91 | 天堂va欧美va亚洲va老司机 | 久久精品久久精品久久 | 日本精品视频免费观看 | 久久高清国产视频 | 午夜久久影院 | 国偷自产视频一区二区久 | 国产麻豆精品久久 | 六月丁香激情网 | 精品在线观看一区二区 | 人人爽久久涩噜噜噜网站 | 国产99亚洲 | 国产黄a三级三级三级三级三级 | 黄网站app在线观看免费视频 | 一区二区三区日韩在线 | 日韩精品久久中文字幕 | 欧美成a人片在线观看久 | 免费观看黄色12片一级视频 | 黄色网大全 | 欧洲av不卡 | 中文字幕日韩国产 | 国产资源网站 | 免费观看成人 | 99 视频 高清 | 色综合久久悠悠 | 欧美成年人在线观看 | 国产精品99久久久久久久久久久久 | 精品视频在线免费 | 天天草天天干天天射 | 国产又粗又猛又色 | 国产精品第54页 | 五月婷婷深开心 | 免费黄a| 日韩精品视频免费在线观看 | 国产在线播放一区二区 | 亚洲高清资源 | 在线观看久 | 婷婷丁香花五月天 | 婷婷色中文 | av网站在线观看免费 | 992tv在线观看网站 | 黄污污网站| 99热精品在线观看 | 成年人免费在线播放 | 成人啪啪18免费游戏链接 | 久久久国产精品人人片99精片欧美一 | 久操伊人| 国产精品欧美久久久久三级 | 中文字幕久久精品亚洲乱码 | 亚洲精品中文在线 | 日韩免费精品 | 久久久久电影网站 | 亚洲高清在线视频 | 9999精品| 欧美一级电影免费观看 | 国内精品久久久久久久97牛牛 | 国产成人三级在线观看 | 97av色| 99精品国产免费久久久久久下载 | 天天躁天天躁天天躁婷 | 国产麻豆果冻传媒在线观看 | 精品国产精品久久 | 欧美性网站 | 国产黄色精品 | 精品久久美女 | 成人网页在线免费观看 | 亚洲精品中文字幕在线观看 | 日本成人中文字幕在线观看 | 911国产| 久久久久久久久久久久国产精品 | 亚洲成人一区 | 亚洲国产人午在线一二区 | 精品亚洲视频在线观看 | 毛片网站免费 | 国产日韩视频在线 | free. 性欧美.com| 免费在线色视频 | 亚洲成人黄色 | 手机看片| 日韩在线视频观看免费 | 在线国产精品一区 | 欧美一级乱黄 | 在线观看激情av | 色播六月天 | 亚洲精品永久免费视频 | 亚洲黄色免费网站 | 久久精品国产一区二区 | 天天综合久久综合 | av不卡免费在线观看 | 人人澡人人模 | 亚洲精品1区2区3区 超碰成人网 | 99精品热 | 国产精品久久久av | 久久久久久久久毛片 | 成人中文字幕av | 国产日韩av在线 | 国产尤物在线 | 一区二区三区日韩精品 | 国产xx在线| 久久久久久久久久国产精品 | 日韩欧美中文 | 国产综合精品久久 | 亚洲在线看 | 东方av免费在线观看 | 久精品视频在线观看 | 亚洲精品自在在线观看 | 五月导航 | 久久免费a | 丁香五月网久久综合 | 午夜在线免费视频 | 在线看一区二区 | 欧美三级在线播放 | 免费看片网址 | 日韩免|