为什么一开始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
,这个接口是用来判断是否丢帧的,每一帧都会调用这个方法(名字起得不好,害得我查了很久才发现这个是每一帧都调用的)。具体方法:
- 每次
DropFrame
时记录一个时间戳,插入到队列中。 - 调用
InputFrameRate
时在队列中查找一个区间,计算这个区间的每帧平均时间,即最大时间戳 减 最小时间戳除以数量。 - 区间的计算:从队列尾部开始,到与尾部时间戳之差小于2秒的最大值,即这个区间最大长度为2秒。
- 最后使用这个每帧平均时间,计算
input_frame_rate
。
为什么后来又升上去了,还没有研究,大概是因为分辨率的降低导致编码速度加快,然后通过MediaOptimization
的估算慢慢提升了帧率。
为什么分辨率会由刚开始的 720x1280 经过几秒后降到 360x640
VideoStreamEncoder::OnFrame
↓
VideoStreamEncoder::MaybeEncodeVideoFrame
↓
// 判断是否应该降低视频能级,如果降级则调用 AdaptDown ,并且跳过该帧的解码
VideoStreamEncoder::DropDueToSize
↓
VideoStreamEncoder::AdaptDown
↓
VideoStreamEncoder::VideoSourceProxy::RequestResolutionLowerThan
↓
VideoSourceInterface::AddOrUpdateSink // 最终改变输入帧率的方法
DegradationPreference
:猜测是协议层由对端设置的,表示使用何种策略降低视频能级。可选值有4种:
- DISABLED
- MAINTAIN_FRAMERATE
- MAINTAIN_RESOLUTION
- 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包的方法,这个方法先计算包的数量,然后计算出来详细的包大小,最后一个包一个包的填充数据并发送。
对每个包
- 如果开启了flexfec,则发送flexfec包
- 如果开启了red,则发送携带ulpfec的red包,调用
SendVideoPacketAsRedMaybeWithUlpfec
- 否则直接发送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::GetNackList
将 missing_sequence_numbers_
集合中的数据转化成一个列表返回。
另一个更新 missing_sequence_numbers_
集合的方法是VCMJitterBuffer::UpdateNackList
,调用顺序:
VCMJitterBuffer::InsertPacket(packet)
↓
VCMJitterBuffer::UpdateNackList(sequence_number)
UpdateNackList
的参数 sequence_number
就是 InsertPacket
的参数 packet
所持有的 sequence_number
。UpdateNackList
根据新插入的 sequence_number
更新 missing_sequence_numbers_
集合。主要逻辑如下:
-
latest_received_sequence_number_
表示最新接收到的 sequence number,这个值会在InsertPacket
和UpdateNackList
中更新。 -
sequence number 应是连续的整数,如果传入的
sequence_number
比latest_received_sequence_number_
要大,也就是时间顺序上要晚,如果两者不是连续的(sequence_number > latest_received_sequence_number_ + 1
),说明它们之间有其他 packet 没有接收到。那么这个区间内的所有 sequence number 就要添加到missing_sequence_numbers_
集合中。 -
如果
sequence_number
比latest_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 标志位表示帧的最后一个数据包的?
网友评论