WebRTC 视频流发送统计报告

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.Capturerframes_dropped_by_capturerDropReason::kSource
DroppedFrames.EncoderQueueframes_dropped_by_encoder_queueDropReason::kEncoderQueue
DroppedFrames.Encoderframes_dropped_by_encoderDropReason::kEncoder
DroppedFrames.Ratelimiterframes_dropped_by_rate_limiterDropReason::kMediaOptimization
DroppedFrames.CongestionWindowframes_dropped_by_congestion_windowDropReason::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()SendStatisticsProxyCpuOveruseMetricsObserver的纯虚函数实现:

@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_msencode_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_。类型是RateCounterRateCounter的实现见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

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值