WebRTC Qos 杂问

为什么一开始fps会降到1,后来有了正常的两方通话后又恢复到30

WebRTC对每一帧调用 VideoStreamEncoder::OnFrame,然后调用VideoStreamEncoder::MaybeEncodeVideoFrame 这个方法中可能会执行最终的编码。

定义了一个 posted_frames_waiting_for_encode_变量表示当前等待编码的帧数,通过它判断是否应该跳过来不及编码的帧。

每次OnFrame调用会在调用的线程执行 posted_frames_waiting_for_encode_++,然后在编码线程中如果判断posted_frames_waiting_for_encode_ > 1则跳过编码,如果posted_frames_waiting_for_encode_ == 1则进行编码。不论是否跳过还是执行了编码,这个值都会减一posted_frames_waiting_for_encode_--。这样就保证了如果有多个帧正在等待编码,则会编码这些帧中的最晚的帧。

如果等待编码的帧有多个,说明编码性能赶不上设备采集帧率,编码器的性能会最终影响fps,低性能会导致实际fps降低。

另一方面,在VideoSender中也通过改变编码器的参数来改变实际的编码帧率,该参数为encoder_params_.input_frame_rate

VideoSender调用VideoSender::UpdateEncoderParameters 更新帧率参数,它调用media_optimization::MediaOptimization对象_mediaOpt
InputFrameRate()方法得到估算的帧率。

MediaOptimization是一个工具类,它的功能之一就是估算input_frame_rate,它会记录每一帧的时间戳,然后根据最近两秒的帧数来估算帧率。

对每一帧,调用MediaOptimization::DropFrame ,这个接口是用来判断是否丢帧的,每一帧都会调用这个方法(名字起得不好,害得我查了很久才发现这个是每一帧都调用的)。具体方法:

  1. 每次DropFrame时记录一个时间戳,插入到队列中。
  2. 调用 InputFrameRate 时在队列中查找一个区间,计算这个区间的每帧平均时间,即最大时间戳 减 最小时间戳除以数量。
  3. 区间的计算:从队列尾部开始,到与尾部时间戳之差小于2秒的最大值,即这个区间最大长度为2秒。
  4. 最后使用这个每帧平均时间,计算 input_frame_rate

为什么后来又升上去了,还没有研究,大概是因为分辨率的降低导致编码速度加快,然后通过MediaOptimization的估算慢慢提升了帧率。

为什么分辨率会由刚开始的 720x1280 经过几秒后降到 360x640

VideoStreamEncoder::OnFrame
↓
VideoStreamEncoder::MaybeEncodeVideoFrame
↓
// 判断是否应该降低视频能级,如果降级则调用 AdaptDown ,并且跳过该帧的解码
VideoStreamEncoder::DropDueToSize
↓
VideoStreamEncoder::AdaptDown
↓
VideoStreamEncoder::VideoSourceProxy::RequestResolutionLowerThan
↓
VideoSourceInterface::AddOrUpdateSink // 最终改变输入帧率的方法

DegradationPreference:猜测是协议层由对端设置的,表示使用何种策略降低视频能级。可选值有4种:

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

根据log判断默认值是 MAINTAIN_FRAMERATE,后面只有在结束通信时设置成了 DISABLED。猜测可能一直保持MAINTAIN_FRAMERATE不变。

VideoStreamEncoder::DropDueToSize 根据一个初始码率encoder_start_bitrate_bps_来限制分辨率。将初始码率分成3个档次对应一个最大分辨率:

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

如果大于该档次的最大分辨率就判断为需要降低分辨率。

DropFrame是如何运作的

MediaOptimization,在两个类中使用:

  • vcm::VideoSender
  • webrtc::VCMEncodedFrameCallback

MediaOptimization内部使用FrameDropper,用来计算什么时候丢帧。


FEC在WEBRTC是怎么使用的?

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

payload,即SDP中定义的负载类型id。在代码中,payload >= 0 表示启用,payload < 0表示关闭。

FlexfecSender flexfec_sender 负责flexfec的对象。google的 demo server 都不支持,估计很少有支持的吧。如果有的话,它是比ulpfec优先的,可以在demo的设置项中打开开关。

red_payload_type_: redundant payload。red用来持有ulpfec,没有red也就没有ulpfec。
ulpfec_payload_type_: ulpfec payload。

RTPSenderVideo::SendVideo() 最终发送视频数RTP包的方法,这个方法先计算包的数量,然后计算出来详细的包大小,最后一个包一个包的填充数据并发送。

对每个包

  1. 如果开启了flexfec,则发送flexfec包
  2. 如果开启了red,则发送携带ulpfec的red包,调用SendVideoPacketAsRedMaybeWithUlpfec
  3. 否则直接发送video数据

为什么H264的时候没有启用

RtpVideoSender::ConfigureProtection
↓
PayloadTypeSupportsSkippingFecPackets
// 它判断了只有vp8和vp9才开启FEC,也就是说h264不开启FEC。

衡量FEC的效果


NACK with H264 为什么会导致 FEC 包的重传?

一个完整的帧包含所有的数据,是不需要重传的,产生 NACK 的原因一定是判定了一帧中的某些包丢失了。那么一定有个机制来判断一帧的完整性,而这种机制对 H264 with FEC 一定是有缺陷的。可能是因为,原始包 + FEC包都到达才判定为完整,因此导致了即使所有的原始数据包都到达,有FEC包没到达,也会被判定为帧不完整。

先要搞清楚两个问题:一是选择哪些包发送NACK?另一个是怎么判断帧的完整性?

哪些包需要发送NACK?

返回 NACK 列表的调用顺序:

ModuleProcessThread
↓
VideoReceiver::Process
↓
VCMReceiver::NackList // 没有计算,直接调用下面的方法
↓
VCMJitterBuffer::GetNackList(bool* request_key_frame) // 执行具体计算的地方

ModuleProcessThread 周期性调用 VideoReceiver::Process 方法,最后调用的 VCMJitterBuffer 中的方法获取 NACK 列表,VCMJitterBuffer 负责计算哪些包是需要发送 NACK 的,也就是确定 NACK 列表。

VCMJitterBuffer::GetNackList 方法先根据时间和缓冲区大小更新missing_sequence_numbers_ 集合,使之不要超出最大限制。主要是根据一些条件删除 missing_sequence_numbers_ 中的数据,但这些判断与 VCMFrameBuffer 的状态没有太大关系。更多的是要判断是否应该 request_key_frame,即请求关键帧。

然后 VCMJitterBuffer::GetNackListmissing_sequence_numbers_ 集合中的数据转化成一个列表返回。

另一个更新 missing_sequence_numbers_ 集合的方法是VCMJitterBuffer::UpdateNackList,调用顺序:

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

UpdateNackList 的参数 sequence_number 就是 InsertPacket 的参数 packet 所持有的 sequence_numberUpdateNackList 根据新插入的 sequence_number 更新 missing_sequence_numbers_ 集合。主要逻辑如下:

  • latest_received_sequence_number_ 表示最新接收到的 sequence number,这个值会在 InsertPacketUpdateNackList 中更新。

  • sequence number 应是连续的整数,如果传入的 sequence_numberlatest_received_sequence_number_ 要大,也就是时间顺序上要晚,如果两者不是连续的(sequence_number > latest_received_sequence_number_ + 1),说明它们之间有其他 packet 没有接收到。那么这个区间内的所有 sequence number 就要添加到 missing_sequence_numbers_ 集合中。

  • 如果sequence_numberlatest_received_sequence_number_ 要小,说明这个 packet 是迟到的一个包,应该在之前的处理过程中,已经添加到了 missing_sequence_numbers_中,因此在 missing_sequence_numbers_ 中删除这个 sequence number 即可。

如何判断帧完整

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

判断 VCMSessionInfo 完整有几个条件:

  • VCMSessionInfo 中有第一个 packet
  • VCMSessionInfo 中有最后一个 packet
  • 所有的 packet 的 sequence number 都是连续的

这个条件很容易理解,也没有什么特别之处,关键在于如何判断是否有最后一个 packet,在代码中由变量 last_packet_seq_num_ 存储最后一个 packet 的 sequence number。

只有 VCMSessionInfo::InsertPacket 方法会更新 last_packet_seq_num_,而其中也明确区分了 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 标识位设置为1,才判定为最后一个包,但看后面的条件 IsNewerSequenceNumber(packet.seqNum, last_packet_seq_num_) 还不一定只有一个包被设置了 M 标识位。

问题:H264 RTP中是如何规定 M 标志位表示帧的最后一个数据包的?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容