WebRTC 视频流发送统计报告

  • Post author:
  • Post category:其他




WebRTC 视频流发送统计报告

在每次视频推流或拉流结束后,WebRTC都会输出本次视频推拉流的统计报告。其中包含了关于评价本次推拉流质量相关的若干参数。本文的主要目的是介绍视频推流相关的统计指标含义。

关于拉流相关的统计指标,请参考我的另外一篇文章

《WebRTC 视频流接收统计报告》

本文源码基于

WebRTC M94

编写。后续 WebRTC 版本可能有所变化,细节可能不同,但基本原理应该适用。



相关源码

  • 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

在这里插入图片描述



版权声明:本文为epubcn原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。