WebRTC Qos 雜問

為什么一開始fps會(huì)降到1,后來有了正常的兩方通話后又恢復(fù)到30

WebRTC對每一幀調(diào)用 VideoStreamEncoder::OnFrame,然后調(diào)用VideoStreamEncoder::MaybeEncodeVideoFrame 這個(gè)方法中可能會(huì)執(zhí)行最終的編碼。

定義了一個(gè) posted_frames_waiting_for_encode_變量表示當(dāng)前等待編碼的幀數(shù),通過它判斷是否應(yīng)該跳過來不及編碼的幀。

每次OnFrame調(diào)用會(huì)在調(diào)用的線程執(zhí)行 posted_frames_waiting_for_encode_++,然后在編碼線程中如果判斷posted_frames_waiting_for_encode_ > 1則跳過編碼,如果posted_frames_waiting_for_encode_ == 1則進(jìn)行編碼。不論是否跳過還是執(zhí)行了編碼,這個(gè)值都會(huì)減一posted_frames_waiting_for_encode_--。這樣就保證了如果有多個(gè)幀正在等待編碼,則會(huì)編碼這些幀中的最晚的幀。

如果等待編碼的幀有多個(gè),說明編碼性能趕不上設(shè)備采集幀率,編碼器的性能會(huì)最終影響fps,低性能會(huì)導(dǎo)致實(shí)際fps降低。

另一方面,在VideoSender中也通過改變編碼器的參數(shù)來改變實(shí)際的編碼幀率,該參數(shù)為encoder_params_.input_frame_rate。

VideoSender調(diào)用VideoSender::UpdateEncoderParameters 更新幀率參數(shù),它調(diào)用media_optimization::MediaOptimization對象_mediaOpt
InputFrameRate()方法得到估算的幀率。

MediaOptimization是一個(gè)工具類,它的功能之一就是估算input_frame_rate,它會(huì)記錄每一幀的時(shí)間戳,然后根據(jù)最近兩秒的幀數(shù)來估算幀率。

對每一幀,調(diào)用MediaOptimization::DropFrame ,這個(gè)接口是用來判斷是否丟幀的,每一幀都會(huì)調(diào)用這個(gè)方法(名字起得不好,害得我查了很久才發(fā)現(xiàn)這個(gè)是每一幀都調(diào)用的)。具體方法:

  1. 每次DropFrame時(shí)記錄一個(gè)時(shí)間戳,插入到隊(duì)列中。
  2. 調(diào)用 InputFrameRate 時(shí)在隊(duì)列中查找一個(gè)區(qū)間,計(jì)算這個(gè)區(qū)間的每幀平均時(shí)間,即最大時(shí)間戳 減 最小時(shí)間戳除以數(shù)量。
  3. 區(qū)間的計(jì)算:從隊(duì)列尾部開始,到與尾部時(shí)間戳之差小于2秒的最大值,即這個(gè)區(qū)間最大長度為2秒。
  4. 最后使用這個(gè)每幀平均時(shí)間,計(jì)算 input_frame_rate

為什么后來又升上去了,還沒有研究,大概是因?yàn)榉直媛实慕档蛯?dǎo)致編碼速度加快,然后通過MediaOptimization的估算慢慢提升了幀率。

為什么分辨率會(huì)由剛開始的 720x1280 經(jīng)過幾秒后降到 360x640

VideoStreamEncoder::OnFrame
↓
VideoStreamEncoder::MaybeEncodeVideoFrame
↓
// 判斷是否應(yīng)該降低視頻能級,如果降級則調(diào)用 AdaptDown ,并且跳過該幀的解碼
VideoStreamEncoder::DropDueToSize
↓
VideoStreamEncoder::AdaptDown
↓
VideoStreamEncoder::VideoSourceProxy::RequestResolutionLowerThan
↓
VideoSourceInterface::AddOrUpdateSink // 最終改變輸入幀率的方法

DegradationPreference:猜測是協(xié)議層由對端設(shè)置的,表示使用何種策略降低視頻能級??蛇x值有4種:

  1. DISABLED
  2. MAINTAIN_FRAMERATE
  3. MAINTAIN_RESOLUTION
  4. BALANCED

根據(jù)log判斷默認(rèn)值是 MAINTAIN_FRAMERATE,后面只有在結(jié)束通信時(shí)設(shè)置成了 DISABLED。猜測可能一直保持MAINTAIN_FRAMERATE不變。

VideoStreamEncoder::DropDueToSize 根據(jù)一個(gè)初始碼率encoder_start_bitrate_bps_來限制分辨率。將初始碼率分成3個(gè)檔次對應(yīng)一個(gè)最大分辨率:

  • [0, 300kbps]=>320x240
  • [300kbps, 500kbps]=>640x480
  • [500kbps, )=>沒有限制,

如果大于該檔次的最大分辨率就判斷為需要降低分辨率。

DropFrame是如何運(yùn)作的

MediaOptimization,在兩個(gè)類中使用:

  • vcm::VideoSender
  • webrtc::VCMEncodedFrameCallback

MediaOptimization內(nèi)部使用FrameDropper,用來計(jì)算什么時(shí)候丟幀。


FEC在WEBRTC是怎么使用的?

rtp_sender_video.cc 文件中處理FEC、NACK等Qos功能。

payload,即SDP中定義的負(fù)載類型id。在代碼中,payload >= 0 表示啟用,payload < 0表示關(guān)閉。

FlexfecSender flexfec_sender 負(fù)責(zé)flexfec的對象。google的 demo server 都不支持,估計(jì)很少有支持的吧。如果有的話,它是比ulpfec優(yōu)先的,可以在demo的設(shè)置項(xiàng)中打開開關(guān)。

red_payload_type_: redundant payload。red用來持有ulpfec,沒有red也就沒有ulpfec。
ulpfec_payload_type_: ulpfec payload。

RTPSenderVideo::SendVideo() 最終發(fā)送視頻數(shù)RTP包的方法,這個(gè)方法先計(jì)算包的數(shù)量,然后計(jì)算出來詳細(xì)的包大小,最后一個(gè)包一個(gè)包的填充數(shù)據(jù)并發(fā)送。

對每個(gè)包

  1. 如果開啟了flexfec,則發(fā)送flexfec包
  2. 如果開啟了red,則發(fā)送攜帶ulpfec的red包,調(diào)用SendVideoPacketAsRedMaybeWithUlpfec
  3. 否則直接發(fā)送video數(shù)據(jù)

為什么H264的時(shí)候沒有啟用

RtpVideoSender::ConfigureProtection
↓
PayloadTypeSupportsSkippingFecPackets
// 它判斷了只有vp8和vp9才開啟FEC,也就是說h264不開啟FEC。

衡量FEC的效果


NACK with H264 為什么會(huì)導(dǎo)致 FEC 包的重傳?

一個(gè)完整的幀包含所有的數(shù)據(jù),是不需要重傳的,產(chǎn)生 NACK 的原因一定是判定了一幀中的某些包丟失了。那么一定有個(gè)機(jī)制來判斷一幀的完整性,而這種機(jī)制對 H264 with FEC 一定是有缺陷的。可能是因?yàn)?,原始?+ FEC包都到達(dá)才判定為完整,因此導(dǎo)致了即使所有的原始數(shù)據(jù)包都到達(dá),有FEC包沒到達(dá),也會(huì)被判定為幀不完整。

先要搞清楚兩個(gè)問題:一是選擇哪些包發(fā)送NACK?另一個(gè)是怎么判斷幀的完整性?

哪些包需要發(fā)送NACK?

返回 NACK 列表的調(diào)用順序:

ModuleProcessThread
↓
VideoReceiver::Process
↓
VCMReceiver::NackList // 沒有計(jì)算,直接調(diào)用下面的方法
↓
VCMJitterBuffer::GetNackList(bool* request_key_frame) // 執(zhí)行具體計(jì)算的地方

ModuleProcessThread 周期性調(diào)用 VideoReceiver::Process 方法,最后調(diào)用的 VCMJitterBuffer 中的方法獲取 NACK 列表,VCMJitterBuffer 負(fù)責(zé)計(jì)算哪些包是需要發(fā)送 NACK 的,也就是確定 NACK 列表。

VCMJitterBuffer::GetNackList 方法先根據(jù)時(shí)間和緩沖區(qū)大小更新missing_sequence_numbers_ 集合,使之不要超出最大限制。主要是根據(jù)一些條件刪除 missing_sequence_numbers_ 中的數(shù)據(jù),但這些判斷與 VCMFrameBuffer 的狀態(tài)沒有太大關(guān)系。更多的是要判斷是否應(yīng)該 request_key_frame,即請求關(guān)鍵幀。

然后 VCMJitterBuffer::GetNackListmissing_sequence_numbers_ 集合中的數(shù)據(jù)轉(zhuǎn)化成一個(gè)列表返回。

另一個(gè)更新 missing_sequence_numbers_ 集合的方法是VCMJitterBuffer::UpdateNackList,調(diào)用順序:

VCMJitterBuffer::InsertPacket(packet)
↓
VCMJitterBuffer::UpdateNackList(sequence_number)

UpdateNackList 的參數(shù) sequence_number 就是 InsertPacket 的參數(shù) packet 所持有的 sequence_number。UpdateNackList 根據(jù)新插入的 sequence_number 更新 missing_sequence_numbers_ 集合。主要邏輯如下:

  • latest_received_sequence_number_ 表示最新接收到的 sequence number,這個(gè)值會(huì)在 InsertPacketUpdateNackList 中更新。

  • sequence number 應(yīng)是連續(xù)的整數(shù),如果傳入的 sequence_numberlatest_received_sequence_number_ 要大,也就是時(shí)間順序上要晚,如果兩者不是連續(xù)的(sequence_number > latest_received_sequence_number_ + 1),說明它們之間有其他 packet 沒有接收到。那么這個(gè)區(qū)間內(nèi)的所有 sequence number 就要添加到 missing_sequence_numbers_ 集合中。

  • 如果sequence_numberlatest_received_sequence_number_ 要小,說明這個(gè) packet 是遲到的一個(gè)包,應(yīng)該在之前的處理過程中,已經(jīng)添加到了 missing_sequence_numbers_中,因此在 missing_sequence_numbers_ 中刪除這個(gè) sequence number 即可。

如何判斷幀完整

VCMJitterBuffer
↓
VCMFrameBuffer.GetState()
↓
VCMSessionInfo.complete()

判斷 VCMSessionInfo 完整有幾個(gè)條件:

  • VCMSessionInfo 中有第一個(gè) packet
  • VCMSessionInfo 中有最后一個(gè) packet
  • 所有的 packet 的 sequence number 都是連續(xù)的

這個(gè)條件很容易理解,也沒有什么特別之處,關(guān)鍵在于如何判斷是否有最后一個(gè) packet,在代碼中由變量 last_packet_seq_num_ 存儲(chǔ)最后一個(gè) packet 的 sequence number。

只有 VCMSessionInfo::InsertPacket 方法會(huì)更新 last_packet_seq_num_,而其中也明確區(qū)分了 H264 和其他 codec:

if (packet.codec == kVideoCodecH264) {
    ...
    if (packet.markerBit &&
        (last_packet_seq_num_ == -1 ||
         IsNewerSequenceNumber(packet.seqNum, last_packet_seq_num_))) {
      last_packet_seq_num_ = packet.seqNum;
    }
  } else {
    ...
  }

注意判斷的條件:packet.markerBit,RTP包的 M 標(biāo)識位設(shè)置為1,才判定為最后一個(gè)包,但看后面的條件 IsNewerSequenceNumber(packet.seqNum, last_packet_seq_num_) 還不一定只有一個(gè)包被設(shè)置了 M 標(biāo)識位。

問題:H264 RTP中是如何規(guī)定 M 標(biāo)志位表示幀的最后一個(gè)數(shù)據(jù)包的?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容