/* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "video/video_quality_observer.h" #include #include #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/strings/string_builder.h" #include "system_wrappers/include/metrics.h" namespace webrtc { namespace { const int kMinFrameSamplesToDetectFreeze = 5; const int kMinCallDurationMs = 3000; const int kMinRequiredSamples = 1; const int kMinIncreaseForFreezeMs = 150; const int kPixelsInHighResolution = 960 * 540; // CPU-adapted HD still counts. const int kPixelsInMediumResolution = 640 * 360; const int kBlockyQpThresholdVp8 = 70; const int kBlockyQpThresholdVp9 = 60; // TODO(ilnik): tune this value. // TODO(ilnik): Add H264/HEVC thresholds. } // namespace VideoQualityObserver::VideoQualityObserver(VideoContentType content_type) : last_frame_decoded_ms_(-1), num_frames_decoded_(0), first_frame_decoded_ms_(-1), last_frame_pixels_(0), last_frame_qp_(0), last_unfreeze_time_(0), time_in_resolution_ms_(3, 0), current_resolution_(Resolution::Low), num_resolution_downgrades_(0), time_in_blocky_video_ms_(0), content_type_(content_type), is_paused_(false) {} VideoQualityObserver::~VideoQualityObserver() { UpdateHistograms(); } void VideoQualityObserver::UpdateHistograms() { // Don't report anything on an empty video stream. if (num_frames_decoded_ == 0) { return; } char log_stream_buf[2 * 1024]; rtc::SimpleStringBuilder log_stream(log_stream_buf); if (last_frame_decoded_ms_ > last_unfreeze_time_) { smooth_playback_durations_.Add(last_frame_decoded_ms_ - last_unfreeze_time_); } std::string uma_prefix = videocontenttypehelpers::IsScreenshare(content_type_) ? "WebRTC.Video.Screenshare" : "WebRTC.Video"; auto mean_time_between_freezes = smooth_playback_durations_.Avg(kMinRequiredSamples); if (mean_time_between_freezes) { RTC_HISTOGRAM_COUNTS_SPARSE_100000(uma_prefix + ".MeanTimeBetweenFreezesMs", *mean_time_between_freezes); log_stream << uma_prefix << ".MeanTimeBetweenFreezesMs " << *mean_time_between_freezes << "\n"; } auto avg_freeze_length = freezes_durations_.Avg(kMinRequiredSamples); if (avg_freeze_length) { RTC_HISTOGRAM_COUNTS_SPARSE_100000(uma_prefix + ".MeanFreezeDurationMs", *avg_freeze_length); log_stream << uma_prefix << ".MeanFreezeDurationMs " << *avg_freeze_length << "\n"; } int64_t call_duration_ms = last_frame_decoded_ms_ - first_frame_decoded_ms_; if (call_duration_ms >= kMinCallDurationMs) { int time_spent_in_hd_percentage = static_cast( time_in_resolution_ms_[Resolution::High] * 100 / call_duration_ms); int time_with_blocky_video_percentage = static_cast(time_in_blocky_video_ms_ * 100 / call_duration_ms); RTC_HISTOGRAM_COUNTS_SPARSE_100(uma_prefix + ".TimeInHdPercentage", time_spent_in_hd_percentage); log_stream << uma_prefix << ".TimeInHdPercentage " << time_spent_in_hd_percentage << "\n"; RTC_HISTOGRAM_COUNTS_SPARSE_100(uma_prefix + ".TimeInBlockyVideoPercentage", time_with_blocky_video_percentage); log_stream << uma_prefix << ".TimeInBlockyVideoPercentage " << time_with_blocky_video_percentage << "\n"; RTC_HISTOGRAM_COUNTS_SPARSE_100( uma_prefix + ".NumberResolutionDownswitchesPerMinute", num_resolution_downgrades_ * 60000 / call_duration_ms); log_stream << uma_prefix << ".NumberResolutionDownswitchesPerMinute " << num_resolution_downgrades_ * 60000 / call_duration_ms << "\n"; } RTC_LOG(LS_INFO) << log_stream.str(); } void VideoQualityObserver::OnDecodedFrame(absl::optional qp, int width, int height, int64_t now_ms, VideoCodecType codec) { if (num_frames_decoded_ == 0) { first_frame_decoded_ms_ = now_ms; last_unfreeze_time_ = now_ms; } ++num_frames_decoded_; if (!is_paused_ && num_frames_decoded_ > 1) { // Process inter-frame delay. int64_t interframe_delay_ms = now_ms - last_frame_decoded_ms_; interframe_delays_.Add(interframe_delay_ms); absl::optional avg_interframe_delay = interframe_delays_.Avg(kMinFrameSamplesToDetectFreeze); // Check if it was a freeze. if (avg_interframe_delay && interframe_delay_ms >= std::max(3 * *avg_interframe_delay, *avg_interframe_delay + kMinIncreaseForFreezeMs)) { freezes_durations_.Add(interframe_delay_ms); smooth_playback_durations_.Add(last_frame_decoded_ms_ - last_unfreeze_time_); last_unfreeze_time_ = now_ms; } else { // Only count inter-frame delay as playback time if there // was no freeze. time_in_resolution_ms_[current_resolution_] += interframe_delay_ms; absl::optional qp_blocky_threshold; // TODO(ilnik): add other codec types when we have QP for them. switch (codec) { case kVideoCodecVP8: qp_blocky_threshold = kBlockyQpThresholdVp8; break; case kVideoCodecVP9: qp_blocky_threshold = kBlockyQpThresholdVp9; break; default: qp_blocky_threshold = absl::nullopt; } if (qp_blocky_threshold && qp.value_or(0) > *qp_blocky_threshold) { time_in_blocky_video_ms_ += interframe_delay_ms; } } } if (is_paused_) { // If the stream was paused since the previous frame, do not count the // pause toward smooth playback. Explicitly count the part before it and // start the new smooth playback interval from this frame. is_paused_ = false; if (last_frame_decoded_ms_ > last_unfreeze_time_) { smooth_playback_durations_.Add(last_frame_decoded_ms_ - last_unfreeze_time_); } last_unfreeze_time_ = now_ms; } int64_t pixels = width * height; if (pixels >= kPixelsInHighResolution) { current_resolution_ = Resolution::High; } else if (pixels >= kPixelsInMediumResolution) { current_resolution_ = Resolution::Medium; } else { current_resolution_ = Resolution::Low; } if (pixels < last_frame_pixels_) { ++num_resolution_downgrades_; } last_frame_decoded_ms_ = now_ms; last_frame_qp_ = qp.value_or(0); last_frame_pixels_ = pixels; } void VideoQualityObserver::OnStreamInactive() { is_paused_ = true; } } // namespace webrtc