/* * Copyright (c) 2023 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 "modules/video_coding/codecs/test/video_codec_stats_impl.h" #include #include "api/numerics/samples_stats_counter.h" #include "api/test/metrics/metrics_logger.h" #include "rtc_base/checks.h" #include "rtc_base/time_utils.h" namespace webrtc { namespace test { namespace { using Frame = VideoCodecStats::Frame; using Stream = VideoCodecStats::Stream; constexpr Frequency k90kHz = Frequency::Hertz(90000); class LeakyBucket { public: LeakyBucket() : level_bits_(0) {} // Updates bucket level and returns its current level in bits. Data is remove // from bucket with rate equal to target bitrate of previous frame. Bucket // level is tracked with floating point precision. Returned value of bucket // level is rounded up. int Update(const Frame& frame) { RTC_CHECK(frame.target_bitrate) << "Bitrate must be specified."; if (prev_frame_) { RTC_CHECK_GT(frame.timestamp_rtp, prev_frame_->timestamp_rtp) << "Timestamp must increase."; TimeDelta passed = (frame.timestamp_rtp - prev_frame_->timestamp_rtp) / k90kHz; level_bits_ -= prev_frame_->target_bitrate->bps() * passed.us() / 1000000.0; level_bits_ = std::max(level_bits_, 0.0); } prev_frame_ = frame; level_bits_ += frame.frame_size.bytes() * 8; return static_cast(std::ceil(level_bits_)); } private: absl::optional prev_frame_; double level_bits_; }; // Merges spatial layer frames into superframes. std::vector Merge(const std::vector& frames) { std::vector superframes; // Map from frame timestamp to index in `superframes` vector. std::map index; for (const auto& f : frames) { if (index.find(f.timestamp_rtp) == index.end()) { index[f.timestamp_rtp] = static_cast(superframes.size()); superframes.push_back(f); continue; } Frame& sf = superframes[index[f.timestamp_rtp]]; sf.width = std::max(sf.width, f.width); sf.height = std::max(sf.height, f.height); sf.frame_size += f.frame_size; sf.keyframe |= f.keyframe; sf.encode_time = std::max(sf.encode_time, f.encode_time); sf.decode_time = std::max(sf.decode_time, f.decode_time); if (f.spatial_idx > sf.spatial_idx) { if (f.qp) { sf.qp = f.qp; } if (f.psnr) { sf.psnr = f.psnr; } } sf.spatial_idx = std::max(sf.spatial_idx, f.spatial_idx); sf.temporal_idx = std::max(sf.temporal_idx, f.temporal_idx); sf.encoded |= f.encoded; sf.decoded |= f.decoded; } return superframes; } Timestamp RtpToTime(uint32_t timestamp_rtp) { return Timestamp::Micros((timestamp_rtp / k90kHz).us()); } SamplesStatsCounter::StatsSample StatsSample(double value, Timestamp time) { return SamplesStatsCounter::StatsSample{value, time}; } TimeDelta CalcTotalDuration(const std::vector& frames) { RTC_CHECK(!frames.empty()); TimeDelta duration = TimeDelta::Zero(); if (frames.size() > 1) { duration += (frames.rbegin()->timestamp_rtp - frames.begin()->timestamp_rtp) / k90kHz; } // Add last frame duration. If target frame rate is provided, calculate frame // duration from it. Otherwise, assume duration of last frame is the same as // duration of preceding frame. if (frames.rbegin()->target_framerate) { duration += 1 / *frames.rbegin()->target_framerate; } else { RTC_CHECK_GT(frames.size(), 1u); duration += (frames.rbegin()->timestamp_rtp - std::next(frames.rbegin())->timestamp_rtp) / k90kHz; } return duration; } } // namespace std::vector VideoCodecStatsImpl::Slice( absl::optional filter) const { std::vector frames; for (const auto& [frame_id, f] : frames_) { if (filter.has_value()) { if (filter->first_frame.has_value() && f.frame_num < *filter->first_frame) { continue; } if (filter->last_frame.has_value() && f.frame_num > *filter->last_frame) { continue; } if (filter->spatial_idx.has_value() && f.spatial_idx != *filter->spatial_idx) { continue; } if (filter->temporal_idx.has_value() && f.temporal_idx > *filter->temporal_idx) { continue; } } frames.push_back(f); } return frames; } Stream VideoCodecStatsImpl::Aggregate(const std::vector& frames) const { std::vector superframes = Merge(frames); RTC_CHECK(!superframes.empty()); LeakyBucket leacky_bucket; Stream stream; for (size_t i = 0; i < superframes.size(); ++i) { Frame& f = superframes[i]; Timestamp time = RtpToTime(f.timestamp_rtp); if (!f.frame_size.IsZero()) { stream.width.AddSample(StatsSample(f.width, time)); stream.height.AddSample(StatsSample(f.height, time)); stream.frame_size_bytes.AddSample( StatsSample(f.frame_size.bytes(), time)); stream.keyframe.AddSample(StatsSample(f.keyframe, time)); if (f.qp) { stream.qp.AddSample(StatsSample(*f.qp, time)); } } if (f.encoded) { stream.encode_time_ms.AddSample(StatsSample(f.encode_time.ms(), time)); } if (f.decoded) { stream.decode_time_ms.AddSample(StatsSample(f.decode_time.ms(), time)); } if (f.psnr) { stream.psnr.y.AddSample(StatsSample(f.psnr->y, time)); stream.psnr.u.AddSample(StatsSample(f.psnr->u, time)); stream.psnr.v.AddSample(StatsSample(f.psnr->v, time)); } if (f.target_framerate) { stream.target_framerate_fps.AddSample( StatsSample(f.target_framerate->millihertz() / 1000.0, time)); } if (f.target_bitrate) { stream.target_bitrate_kbps.AddSample( StatsSample(f.target_bitrate->bps() / 1000.0, time)); int buffer_level_bits = leacky_bucket.Update(f); stream.transmission_time_ms.AddSample( StatsSample(buffer_level_bits * rtc::kNumMillisecsPerSec / f.target_bitrate->bps(), RtpToTime(f.timestamp_rtp))); } } TimeDelta duration = CalcTotalDuration(superframes); DataRate encoded_bitrate = DataSize::Bytes(stream.frame_size_bytes.GetSum()) / duration; int num_encoded_frames = stream.frame_size_bytes.NumSamples(); Frequency encoded_framerate = num_encoded_frames / duration; absl::optional bitrate_mismatch_pct; if (auto target_bitrate = superframes.begin()->target_bitrate; target_bitrate) { bitrate_mismatch_pct = 100.0 * (encoded_bitrate.bps() - target_bitrate->bps()) / target_bitrate->bps(); } absl::optional framerate_mismatch_pct; if (auto target_framerate = superframes.begin()->target_framerate; target_framerate) { framerate_mismatch_pct = 100.0 * (encoded_framerate.millihertz() - target_framerate->millihertz()) / target_framerate->millihertz(); } for (auto& f : superframes) { Timestamp time = RtpToTime(f.timestamp_rtp); stream.encoded_bitrate_kbps.AddSample( StatsSample(encoded_bitrate.bps() / 1000.0, time)); stream.encoded_framerate_fps.AddSample( StatsSample(encoded_framerate.millihertz() / 1000.0, time)); if (bitrate_mismatch_pct) { stream.bitrate_mismatch_pct.AddSample( StatsSample(*bitrate_mismatch_pct, time)); } if (framerate_mismatch_pct) { stream.framerate_mismatch_pct.AddSample( StatsSample(*framerate_mismatch_pct, time)); } } return stream; } void VideoCodecStatsImpl::AddFrame(const Frame& frame) { FrameId frame_id{.timestamp_rtp = frame.timestamp_rtp, .spatial_idx = frame.spatial_idx}; RTC_CHECK(frames_.find(frame_id) == frames_.end()) << "Frame with timestamp_rtp=" << frame.timestamp_rtp << " and spatial_idx=" << frame.spatial_idx << " already exists"; frames_[frame_id] = frame; } Frame* VideoCodecStatsImpl::GetFrame(uint32_t timestamp_rtp, int spatial_idx) { FrameId frame_id{.timestamp_rtp = timestamp_rtp, .spatial_idx = spatial_idx}; if (frames_.find(frame_id) == frames_.end()) { return nullptr; } return &frames_.find(frame_id)->second; } } // namespace test } // namespace webrtc