WebRTC Qos 杂问

作者: 拾识物者 | 来源:发表于2018-08-28 10:34 被阅读130次

    为什么一开始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 标志位表示帧的最后一个数据包的?

    相关文章

      网友评论

        本文标题:WebRTC Qos 杂问

        本文链接:https://www.haomeiwen.com/subject/smptwftx.html