WebRTC 视频流发送统计报告
在每次视频推流或拉流结束后,WebRTC都会输出本次视频推拉流的统计报告。其中包含了关于评价本次推拉流质量相关的若干参数。本文的主要目的是介绍视频推流相关的统计指标含义。
关于拉流相关的统计指标,请参考我的另外一篇文章
《WebRTC 视频流接收统计报告》
。
本文源码基于
WebRTC M94
编写。后续 WebRTC 版本可能有所变化,细节可能不同,但基本原理应该适用。
文章目录
-
WebRTC 视频流发送统计报告
-
-
相关源码
-
统计指标
-
-
BitrateSentInBps
-
{Fec, Media, Padding, Retransmitted, Rtx} BitrateSentInBps
-
DroppedFrames.{Capturer, EncoderQueue, Encoder, Ratelimiter, CongestionWindow}
-
EncodeTimeInMs
-
Frames encoded
-
InputWidthInPixels, InputHeightInPixels
-
SentWidthInPixels, SentHeightInPixels
-
InputFramesPerSecond, SentFramesPerSecond
-
KeyFramesSentInPermille
-
NumberOfPauseEvents
-
PausedTimeInPercent
-
SentToInputFpsRatioPercent
-
SentPacketsLostInPercent
-
-
相关源码
- video\send_statistics_proxy.cc
- video\video_stream_encoder.cc
- call\video_send_stream.cc
- call\rtp_config.cc
- modules\rtp_rtcp\source\rtp_sender_egress.cc
- modules\video_coding\utility\frame_dropper.cc
- modules\congestion_controller\goog_cc\goog_cc_network_control.cc
- media\base\video_adapter.cc
统计指标
BitrateSentInBps
发送码率统计信息。对应变量
total_byte_counter_
。变量类型
RateAccCounter
的实现位于
video\stats_counter.cc
。输出内容举例如下:
periodic_samples:762, {min:140016, avg:201872, max:329176}
RateAccCounter
继承于
StatsCounter
(它还有派生了其他一些 Counter),如下图:
@startuml
class Samples
class StatsCounter
class AvgCounter
class MaxCounter
class PercentCounter
class PermilleCounter
class RateCounter
class RateAccCounter #ffcc00
StatsCounter *-- Samples
StatsCounter <|-- AvgCounter
StatsCounter <|-- MaxCounter
StatsCounter <|-- PercentCounter
StatsCounter <|-- PermilleCounter
StatsCounter <|-- RateCounter
StatsCounter <|-- RateAccCounter
@enduml
这里简单解释一下
RateAccCounter
的计算,举个例子:
// | * * * | * * | * * * | ...
// | Set(100) Set(200) Set(300) | Set(400) Set(600) | Set(650) Set(800) Set(850) |
// |<------ 2 sec ------->| | |
// GetMetric | (300 - 0) / 2 | (600 - 300) / 2 | (850 - 600) / 2 |
针对上面的测试数据,我编写了如下测试代码:
const int kDefaultProcessIntervalMs = 2000;
const uint32_t kStreamId = 123456;
SimulatedClock clock_;
class StatsCounterObserverImpl : public StatsCounterObserver {
public:
StatsCounterObserverImpl() : num_calls_(0), last_sample_(-1) {}
void OnMetricUpdated(int sample) override {
++num_calls_;
last_sample_ = sample;
}
int num_calls_;
int last_sample_;
};
void TestRateAccCounter() {
StatsCounterObserverImpl* observer = new StatsCounterObserverImpl();
RateAccCounter counter(&clock_, observer, true);
counter.Set(100, kStreamId);
counter.Set(200, kStreamId);
counter.Set(300, kStreamId);
clock_.AdvanceTimeMilliseconds(kDefaultProcessIntervalMs); //增加一个计数间隔(2000ms)
counter.ProcessAndGetStats();
printf("num_calls:%d, last_sample_:%d\n", observer->num_calls_, observer->last_sample_);
counter.Set(400, kStreamId);
counter.Set(600, kStreamId);
clock_.AdvanceTimeMilliseconds(kDefaultProcessIntervalMs);
counter.ProcessAndGetStats();
printf("num_calls:%d, last_sample_:%d\n", observer->num_calls_, observer->last_sample_);
counter.Set(650, kStreamId);
counter.Set(800, kStreamId);
counter.Set(850, kStreamId);
clock_.AdvanceTimeMilliseconds(kDefaultProcessIntervalMs);
AggregatedStats stats = counter.ProcessAndGetStats();
printf("num_calls:%d, last_sample_:%d\nstats:%s\n", observer->num_calls_, observer->last_sample_,
stats.ToStringWithMultiplier(1).c_str());
}
使用上面测试代码的运行结果:
num_calls:1, last_sample_:150
num_calls:2, last_sample_:150
num_calls:3, last_sample_:125
stats:periodic_samples:3, {min:125, avg:142, max:150}
我们将上面设置的数值假想为设置的发送字节数(KB),第一个计算间隔(默认2秒)到达时,设置发送了300KB,那么发送码率本次采样值就是(300-0)/2 =150KB/s。继续下次,又一个2秒到达时,发送的字节数是600KB,则本次发送码率是(600-300)/2=150KB/s。继续下次2秒到达时,发送字节数是850KB,发送码率是(850-600)/2=125KB/s。因此,总共采样了3次,最小码率是第3次的125KB/s,最大的是前2次的150KB/s。平均值就是(150+150+125)/3=142KB/s。
OK,有了上面对
RateAccCounter
的认识,理解
BitrateSentInBps
就很容易了:在
SendStatisticsProxy::DataCountersUpdated()
中会不断更新记录当前发送的字节数(
total_byte_counter_.Set(TotalBytes)
),最后结束的时候输出整个发送阶段的度量数据,包括:多少次采样(默认2000ms一次),单个采样间隔记录的最小、最大发送比特数,以及整个发送阶段的平均比特数。(
total_byte_counter_.Set()记录的是Byte,输出内容时,ToStringWithMultiplier(8)输出的是Bit
)
值得注意的是,
BitrateSentInBps
包含了重传包和FEC包的统计。见(
RtpSenderEgress::UpdateRtpStats
的实现)
{Fec, Media, Padding, Retransmitted, Rtx} BitrateSentInBps
这几项都是不同类型的用于度量视频发送数据的,所以放到一起来描述。
他们分别对应
SendStatisticsProxy
的变量
fec_byte_counter_, media_byte_counter_, padding_byte_counter_, retransmit_byte_counter_和rtx_byte_counter_
。变量类型均是
RateAccCounter
(在说明
BitrateSentInBps
的时候介绍过这个类型)。与
BitrateSentInBps
一样,在
SendStatisticsProxy::DataCountersUpdated()
更新其数值。输出内容举例如下:
WebRTC.Video.MediaBitrateSentInBps periodic_samples:762, {min:65296, avg:183232, max:235928}
WebRTC.Video.PaddingBitrateSentInBps periodic_samples:762, {min:0, avg:0, max:0}
WebRTC.Video.RetransmittedBitrateSentInBps periodic_samples:762, {min:0, avg:584, max:79728}
WebRTC.Video.RtxBitrateSentInBps periodic_samples:761, {min:0, avg:584, max:104840}
WebRTC.Video.FecBitrateSentInBps periodic_samples:762, {min:0, avg:13592, max:147416}
关键的调用时序:
@startuml
Actor actor
actor -> RtpSenderEgress : SendPacket
RtpSenderEgress -> RtpSenderEgress : UpdateRtpStats
RtpSenderEgress -> SendStatisticsProxy : DataCountersUpdated
@enduml
Fec
如果发包类型为
RtpPacketMediaType::kForwardErrorCorrection
时,会累计发送的FEC字节数(包括header, payload, padding)
Retransmitted
如果发包类型为
RtpPacketMediaType::kRetransmission
时,会累计重传包字节数
Media, Rtx
RtpConfig
存在3个根据发送包的
ssrc
确定包类型的方法:
bool IsMediaSsrc(uint32_t ssrc) const;
bool IsRtxSsrc(uint32_t ssrc) const;
bool IsFlexfecSsrc(uint32_t ssrc) const;
如果
IsMediaSsrc
返回是 true 则增加
media_byte_counter_
采样记录,如果
IsRtxSsrc
返回是 true 则增加
rtx_byte_counter_
采样记录。
IsFlexfecSsrc
目前尚未用于实际度量输出,应该未来会继续扩展。
Padding
计算总发送码率统计信息
BitrateSentInBps
时,它对应
StreamDataCounters
的成员变量
transmitted
,包括三部分数据:
size_t header_bytes; // Number of bytes used by RTP headers.
size_t payload_bytes; // Payload bytes, excluding RTP headers and padding.
size_t padding_bytes; // Number of padding bytes.
其中的
padding_bytes
对应的就是度量数据
Padding
。它是
RTP协议
中的一部分,解释如下:
padding §: 1 bit
If the padding bit is set, the packet contains one or more additional padding octets at the end which are not part of the
payload. The last octet of the padding contains a count of how many padding octets should be ignored, including itself. Padding may be needed by some encryption algorithms with fixed block sizes or for carrying several RTP packets in a lower-layer protocol data unit.如果设置了该字段,报文的末尾会包含一个或多个填充字节,这些填充字节不是 payload 的内容。最后一个填充字节标识了总共需要忽略多少个填充字节(包括自己)。Padding 可能会被一些加密算法使用,因为有些加密算法需要定长的数据块。Padding 也可能被一些更下层的协议使用,用来一次发送多个 RTP 包。
DroppedFrames.{Capturer, EncoderQueue, Encoder, Ratelimiter, CongestionWindow}
这几种都是丢帧的统计,只不过触发点不同,因此把它们放到一起来描述。它们对应的变量关系如下:
度量属性 | 变量名(VideoSendStream::Stats) | 丢帧原因(枚举) |
---|---|---|
DroppedFrames.Capturer | frames_dropped_by_capturer | DropReason::kSource |
DroppedFrames.EncoderQueue | frames_dropped_by_encoder_queue | DropReason::kEncoderQueue |
DroppedFrames.Encoder | frames_dropped_by_encoder | DropReason::kEncoder |
DroppedFrames.Ratelimiter | frames_dropped_by_rate_limiter | DropReason::kMediaOptimization |
DroppedFrames.CongestionWindow | frames_dropped_by_congestion_window | DropReason::kCongestionWindow |
注:早期的 WebRTC 不存在
DroppedFrames.CongestionWindow
这个度量属性。
这几种丢帧原因,顾名思义,通常就大概明白它的含义了。它们均是在
SendStatisticsProxy::OnFrameDropped()
被调用的时候进行累加。这个方法的调用基本上都是来自
VideoStreamEncoder
:
@startuml
Actor actor
actor -> VideoStreamEncoder : OnFrame
VideoStreamEncoder -> SendStatisticsProxy : OnFrameDropped(DropReason)
note right
DropReason::kCongestionWindow
DropReason::kEncoderQueue
end note
actor -> VideoStreamEncoder : OnDiscardedFrame
VideoStreamEncoder -> SendStatisticsProxy : OnFrameDropped(DropReason)
note right
DropReason::kSource
end note
actor -> VideoStreamEncoder : MaybeEncodeVideoFrame
VideoStreamEncoder -> SendStatisticsProxy : OnFrameDropped(DropReason)
note right
DropReason::kEncoderQueue
end note
actor -> VideoStreamEncoder : OnDroppedFrame
VideoStreamEncoder -> SendStatisticsProxy : OnFrameDropped(DropReason)
note right
DropReason::kMediaOptimization
DropReason::kEncoder
end note
@enduml
DropReason::kSource
可以理解为视频帧采集后,因为分辨率像素值约束、帧率控制的一些需要,而主动丢弃。可以参看
VideoAdapter::AdaptFrameResolution()
的具体实现,源码位置:
media\base\video_adapter.cc
DropReason::kEncoderQueue
视频编码器由于各种原因阻塞、暂停,或当前视频帧分辨率过大超过允许的码率范围等原因,而主动丢弃。
DropReason::kEncoder
可以理解因为由于视频编码器内部的码率控制器(RateLimiter)触发的主动丢帧。具体可以参考
FrameEncodeMetadataWriter::ExtractEncodeStartTimeAndFillMetadata()
和
FrameEncodeMetadataWriter::OnEncodeStarted()
这两个方法,源码位置:
video\frame_encode_metadata_writer.cc
DropReason::kMediaOptimization
FrameDropper
是WebRTC比较经典的一个类,存在多年,它利用漏桶算法来实现码率控制。源码位于
modules\video_coding\utility\frame_dropper.cc
。网络上关于
FrameDropper
的介绍文字多如牛毛,这里就不赘述了。
DropReason::kMediaOptimization
主要是当
FrameDropper::DropFrame()
返回TRUE时触发,见
VideoStreamEncoder::MaybeEncodeVideoFrame()
中的实现。
DropReason::kCongestionWindow
congestion
的中文解释是“拥塞”,所以
CongestionWindow
的直译是“拥塞窗口”。这个 DropReason 是在2020年2月7日提交上来的,commit id 是
9b881abea95736a8aec8f1933d4c88aa452d88e9
,下面是 commit message:
Enable congestion window pushback to reduce bitrate by only drop video frames.
With current congestion window pushback, when congestion window is filling up, it will reduce bitrate directly and encoder may reduce encode quality, resolution, or framerate to adapt to the allocated bitrate, the behavior is depending on the degradation preference.
This change enable congestion window to only drop frames to reduce bitrate (when needed) instead of reduce general bitrate allocation.
我的理解是在需要减少编码器码率的时候,通过丢帧来达到减少码率的目的,而不是按照 degradation preference 来让编码器降低编码质量、分辨率或帧率以适应新的码率分配。
这种丢帧策略目前默认是不开启的,如果需要,可以通过 Field Trials 来设置,它的定义在
rtc_base\experiments\rate_control_settings.h
,如下:
struct CongestionWindowConfig {
static constexpr char kKey[] = "WebRTC-CongestionWindow";
absl::optional<int> queue_size_ms;
absl::optional<int> min_bitrate_bps;
absl::optional<DataSize> initial_data_window;
bool drop_frame_only = false;
std::unique_ptr<StructParametersParser> Parser();
static CongestionWindowConfig Parse(absl::string_view config);
};
例如:
"WebRTC-CongestionWindow/InitWin:100000/"
"WebRTC-CongestionWindow/DropFrame:true/"
"WebRTC-CongestionWindow/QueueSize:800,MinBitrate:30000,DropFrame:true/"
"WebRTC-CongestionWindow/QueueSize:100,MinBitrate:30000/"
......
更多的设置例子可以参考
modules\congestion_controller\goog_cc\goog_cc_network_control_unittest.cc
EncodeTimeInMs
单帧平均编码耗时(毫秒)
。对应变量
encode_time_counter_
,类型
SampleCounter
。在
SendStatisticsProxy::OnEncodedFrameTimeMeasured()
调用时增加计数。
OnEncodedFrameTimeMeasured()
是
SendStatisticsProxy
对
CpuOveruseMetricsObserver
的纯虚函数实现:
@startuml
interface CpuOveruseMetricsObserver {
+OnEncodedFrameTimeMeasured
}
class VideoStreamEncoderObserver
class SendStatisticsProxy
CpuOveruseMetricsObserver <|.. VideoStreamEncoderObserver
VideoStreamEncoderObserver <|-- SendStatisticsProxy
@enduml
它会传来两个参数,一个是单帧编码耗时
encode_duration_ms
,一个是当前的编码CPU使用度
encode_usage_percent_
。
编码的主要调用时序如下:
@startuml
Actor encoder
encoder -> VideoStreamEncoder : OnEncodedImage
VideoStreamEncoder -> VideoStreamEncoder : RunPostEncode
VideoStreamEncoder -> VideoStreamEncoderResourceManager : OnEncodeCompleted
VideoStreamEncoderResourceManager -> EncodeUsageResource : OnEncodedCompleted
EncodeUsageResource -> OveruseFrameDetector : FrameSent
OveruseFrameDetector -> OveruseFrameDetector : EncodedFrameTimeMeasured
OveruseFrameDetector -> SendStatisticsProxy : OnEncodedFrameTimeMeasured
@enduml
一帧的编码时间,原始数据是来自
EncodedImage::Timing
中的
encoded_start_ms
和
encode_finish_ms
两个值相减。见
VideoStreamEncoder::RunPostEncode()
的实现:
absl::optional<int> encode_duration_us;
if (encoded_image.timing_.flags != VideoSendTiming::kInvalid) {
encode_duration_us =
// TODO(nisse): Maybe use capture_time_ms_ rather than encode_start_ms_?
TimeDelta::Millis(encoded_image.timing_.encode_finish_ms -
encoded_image.timing_.encode_start_ms)
.us();
}
最终,利用
SampleCounter::Avg()
方法完成平均编码耗时的计算。值得一提的是,
SampleCounter::Avg()
并不是直接将“总编码耗时时间÷次数”得到平均值,使用的计算公式是:(总耗时 + (次数/2))/次数,这个会比直接计算得到值大0.5,不知道出于什么考虑?四舍五入?因为计算结果是要强转成整型。
Frames encoded
顾名思义,编码帧数总数量。在
SendStatisticsProxy::OnSendEncodedImage()
中更新。调用时序:
@startuml
Actor actor
actor -> VideoStreamEncoder : OnEncodedImage
VideoStreamEncoder -> SendStatisticsProxy : OnSendEncodedImage
@enduml
InputWidthInPixels, InputHeightInPixels
平均输入视频宽高
。对应于
SendStatisticsProxy
的成员变量
input_width_counter_, input_height_counter_
,类型是
SampleCounter
。在
SendStatisticsProxy::OnIncomingFrame()
方法中增加输入宽高的采样值。
SentWidthInPixels, SentHeightInPixels
平均发送视频宽高
。也可以理解为平均编码视频宽高。对应于
SendStatisticsProxy
的成员变量
sent_width_counter_, sent_height_counter_
,类型是
SampleCounter
。在
SendStatisticsProxy::UmaSamplesContainer::RemoveOld()
方法中增加采样值。
主要的调用时序:
@startuml
Actor actor
actor -> VideoStreamEncoder : OnEncodedImage
VideoStreamEncoder -> SendStatisticsProxy : OnSendEncodedImage
SendStatisticsProxy -> UmaSamplesContainer : InsertEncodedFrame
UmaSamplesContainer -> UmaSamplesContainer : RemoveOld
@enduml
SendStatisticsProxy
有一个类型为
EncodedFrameMap
的成员变量
encoded_frames_
,Key是视频帧时间戳,Value是
SendStatisticsProxy::Frame
。最大存储
150
帧数据(
kMaxEncodedFrameMapSize
)。每次编码一帧后,如果满足条件(Map没满、两帧时间差没超过10秒),就记录或更新到
encoded_frames_
中。
在记录之前,会调用
RemoveOld()
方法,从
encoded_frames_
中删除距离当前时间800毫秒(
kMaxEncodedFrameWindowMs
)以上的帧。并将删除的帧的宽高记录到
sent_width_counter_, sent_height_counter_
。因此,平均发送视频宽高的数据来源就来自于每一个编码帧的宽高。
InputFramesPerSecond, SentFramesPerSecond
输入和发送帧率统计信息。输出内容举例:
WebRTC.Video.InputFramesPerSecond periodic_samples:763, {min:15, avg:15, max:17}
WebRTC.Video.SentFramesPerSecond periodic_samples:762, {min:6, avg:15, max:16}
对应于
SendStatisticsProxy
的成员变量
input_fps_counter_
和
sent_fps_counter_
。类型是
RateCounter
。
RateCounter
的实现见
video\stats_counter.cc
,它是以 2 秒(
kDefaultProcessIntervalMs
)为单位来计算帧率的。
输入帧率没什么太多可说的,主要是根据设置的采集帧率,从视频输入设备获取到的真实帧率。
发送帧率则通常因为各种原因,要小于输入帧率。它的值在
SendStatisticsProxy::UmaSamplesContainer::InsertEncodedFrame()
中更新,也就是表示每编码一帧,假如编码帧的时间戳不同于已记录的,就增加1。实际中,由于运行环境的复杂性,往往输入的帧无法达到100%都编码,所以
SentFramesPerSecond
往往是小等于
InputFramesPerSecond
,越接近,表示本次的编码过程运行的越好。
KeyFramesSentInPermille
关键帧千分率
。对应于
SendStatisticsProxy
的成员变量
key_frame_counter_
。类型是
BoolSampleCounter
。在
SendStatisticsProxy::OnSendEncodedImage()
中如果判断当前编码帧类型是
VideoFrameType::kVideoFrameKey
则增加采样计数。最后调用
BoolSampleCounter::Permille()
方法计算关键帧占所有编码帧的千分率(会强转为整数)。
NumberOfPauseEvents
表示编码器目标编码码率在“0”与“非0”之间变化的次数
。比如上次的目标码率不是 0,下一次变为了 0,则
NumberOfPauseEvents
的值加1。如果下次由 0 变为不是 0,则
NumberOfPauseEvents
再加1。如此类推。
PausedTimeInPercent
对应变量
paused_time_counter_
,类型
BoolSampleCounter
。
每次
SendStatisticsProxy::OnSetEncoderTargetRate()
被调用时,将当前时间和上次该函数调用时间的差,记录到
paused_time_counter_
进行累加(
BoolSampleCounter
的成员变量
num_samples
)。其中,如果
VideoStream::Stats::target_media_bitrate_bps
为 0 时,会单独累加记录到一个独立变量(
BoolSampleCounter
的成员变量
sum
)。
VideoStream::Stats::target_media_bitrate_bps
的注释为:
Bitrate the encoder is currently configured to use due to bandwidth limitations
,即:
在给定带宽下视频编码器的可用码率
。
最后,调用
BoolSampleCounter::Percent()
计算得到
PausedTimeInPercent
。因此,这个数值表示
分配给编码器的目标码率为0的时间占比
。比例越大,表示编码器存在越多的时间没有目标编码码率,这段时间将不会编码,也不会有数据发送。
下面是估算码率的调用时序(主要部分):
@startuml
Actor actor
actor -> RtpTransportControllerSend : UpdateControlState
RtpTransportControllerSend -> Call : OnTargetTransferRate
Call -> BitrateAllocator : OnNetworkEstimateChanged
BitrateAllocator -> VideoSendStreamImpl : OnBitrateUpdated
VideoSendStreamImpl-> SendStatisticsProxy : OnSetEncoderTargetRate(bitrate_bps)
actor -> VideoSendStreamImpl : StopVideoSendStream
VideoSendStreamImpl-> SendStatisticsProxy : OnSetEncoderTargetRate(0)
@enduml
关于 WebRTC 的在拥塞控制下的码率分配分析文章有很多,随便一搜一大堆,例如
这篇文章
,这里就不再赘述相关细节了。
SentToInputFpsRatioPercent
**平均发送帧率与平均输入帧率占比。**计算公式:
(100 * sent_fps_avg + in_fps_avg / 2) / in_fps_avg
。最大值100。
数值越大,表示平均发送帧率越接近于平均输入帧率。
数值越小,表示有越多的输入帧没有发送出去,这就需要结合其他度量指判断具体的原因了。
SentPacketsLostInPercent
发送丢包率。根据
ReportBlockStats::FractionLostInPercent()
方法得到。在
SendStatisticsProxy::OnReportBlockDataUpdated()
中会更新丢包数,用于丢包率的计算。丢包数肯定是来自 RTCP 里拉,这个搞音视频人的都懂的,不多解释了。主要的调用时序:
@startuml
Actor actor
actor -> RtpVideoSender : DeliverRtcp
RtpVideoSender -> ModuleRtpRtcpImpl : IncomingRtcpPacket
ModuleRtpRtcpImpl -> RTCPReceiver : IncomingPacket
RTCPReceiver -> RTCPReceiver : TriggerCallbacksFromRtcpPacket
RTCPReceiver -> SendStatisticsProxy : OnReportBlockDataUpdated
@enduml