blob: e3191bcb1ce2730c1b17cea90e33cdf5e731ebe8 [file] [log] [blame]
/*
* Copyright (c) 2026 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 <algorithm>
#include <cstddef>
#include <optional>
#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/functional/any_invocable.h"
#include "api/numerics/samples_stats_counter.h"
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/moving_percentile_filter.h"
#include "video/timing/simulator/frame_base.h"
#ifndef VIDEO_TIMING_SIMULATOR_STREAM_BASE_H_
#define VIDEO_TIMING_SIMULATOR_STREAM_BASE_H_
namespace webrtc::video_timing_simulator {
// CRTP base struct for code reuse.
template <typename StreamT, typename FrameT>
struct StreamBase {
// Data members are defined in derived struct.
// -- CRTP accessors --
const StreamT& self() const { return static_cast<const StreamT&>(*this); }
StreamT& self() { return static_cast<StreamT&>(*this); }
// -- Helpers --
bool IsEmpty() const { return self().frames.empty(); }
// -- Per-frame metric population --
void PopulateFrameDelayVariations(float baseline_percentile = 0.0,
size_t baseline_window_size = 300) {
if (IsEmpty()) {
return;
}
SortByArrivalOrder(self().frames);
// One-way delay measurement offsets.
Timestamp arrival_offset = Timestamp::PlusInfinity();
Timestamp departure_offset = Timestamp::PlusInfinity();
for (const auto& frame : self().frames) {
Timestamp arrival = frame.ArrivalTimestamp();
Timestamp departure = frame.DepartureTimestamp();
if (arrival.IsFinite() && departure.IsFinite()) {
arrival_offset = arrival;
departure_offset = departure;
break;
}
}
if (!arrival_offset.IsFinite() || !departure_offset.IsFinite()) {
RTC_LOG(LS_WARNING)
<< "Did not find valid arrival and/or departure offsets";
return;
}
// The baseline filter measures the minimum (by default) one-way delay
// seen over a window. The corresponding value is then used to anchor all
// other one-way delay measurements, creating the frame delay variation.
MovingPercentileFilter<TimeDelta> baseline_filter(baseline_percentile,
baseline_window_size);
// Calculate frame delay variations relative the moving baseline.
for (auto& frame : self().frames) {
TimeDelta one_way_delay =
frame.OneWayDelay(arrival_offset, departure_offset);
baseline_filter.Insert(one_way_delay);
frame.frame_delay_variation =
one_way_delay - baseline_filter.GetFilteredValue();
}
}
// -- Per stream-metric aggregation --
// Count number of set and true booleans accessed through `accessor`.
int CountSetAndTrue(absl::AnyInvocable<std::optional<bool>(const FrameT&)
const> accessor) const {
return absl::c_count_if(
self().frames, [accessor = std::move(accessor)](const auto& frame) {
std::optional<bool> value = accessor(frame);
return value.has_value() && *value;
});
}
// Count number of finite timestamps accessed through `accessor`.
int CountFiniteTimestamps(
absl::AnyInvocable<Timestamp(const FrameT&) const> accessor) const {
return absl::c_count_if(
self().frames, [accessor = std::move(accessor)](const auto& frame) {
return accessor(frame).IsFinite();
});
}
// Sum non-negative int field values accessed through `accessor`.
int SumNonNegativeIntField(
absl::AnyInvocable<int(const FrameT&) const> accessor) const {
return absl::c_accumulate(
self().frames, 0,
[accessor = std::move(accessor)](int sum, const auto& frame) {
int value = accessor(frame);
RTC_DCHECK_GE(value, 0);
return sum + value;
});
}
// Build samples of positive int field values accessed through `accessor`.
SamplesStatsCounter BuildSamplesPositiveInt(
absl::AnyInvocable<int(const FrameT&) const> accessor) const {
SamplesStatsCounter stats(self().frames.size());
for (const auto& frame : self().frames) {
int value = accessor(frame);
RTC_DCHECK_GT(value, 0);
stats.AddSample({.value = static_cast<double>(value),
.time = Timestamp::PlusInfinity()});
}
return stats;
}
// Build samples of all set and finite TimeDelta field values accessed
// through `accessor`.
SamplesStatsCounter BuildSamplesMs(
absl::AnyInvocable<std::optional<TimeDelta>(const FrameT&) const>
accessor) const {
SamplesStatsCounter stats(self().frames.size());
for (const auto& frame : self().frames) {
std::optional<TimeDelta> value = accessor(frame);
if (!value.has_value() || !value->IsFinite()) {
continue;
}
stats.AddSample(
{.value = value->ms<double>(), .time = Timestamp::PlusInfinity()});
}
return stats;
}
// Build samples of all TimeDelta inter-calculated metrics provided by
// `calculator`.
SamplesStatsCounter BuildSamplesMs(
absl::AnyInvocable<TimeDelta(const FrameT&, const FrameT&) const>
calculator) const {
SamplesStatsCounter stats(self().frames.size());
for (size_t i = 1; i < self().frames.size(); ++i) {
const auto& cur = self().frames[i];
const auto& prev = self().frames[i - 1];
TimeDelta inter = calculator(cur, prev);
if (!inter.IsFinite()) {
continue;
}
stats.AddSample(
{.value = inter.ms<double>(), .time = Timestamp::PlusInfinity()});
}
return stats;
}
// Duration between min and max finite timestamps accessed through
// `accessor`.
TimeDelta MinMaxDuration(
absl::AnyInvocable<Timestamp(const FrameT&) const> accessor) const {
if (IsEmpty()) {
return TimeDelta::PlusInfinity();
}
Timestamp min_value = Timestamp::PlusInfinity();
Timestamp max_value = Timestamp::MinusInfinity();
for (const auto& frame : self().frames) {
Timestamp time = accessor(frame);
if (!time.IsFinite()) {
continue;
}
min_value = std::min(min_value, time);
max_value = std::max(time, max_value);
}
if (!min_value.IsFinite() || !max_value.IsFinite()) {
return TimeDelta::PlusInfinity();
}
RTC_DCHECK_LE(min_value, max_value);
return max_value - min_value;
}
// -- Per-stream metrics --
// Duration between first and last departed frames.
TimeDelta DepartureDuration() const {
return MinMaxDuration(
[](const auto& frame) { return frame.DepartureTimestamp(); });
}
// Duration between first and last arrived frames.
TimeDelta ArrivalDuration() const {
return MinMaxDuration(
[](const auto& frame) { return frame.ArrivalTimestamp(); });
}
// Total number of assembled frames.
int NumAssembledFrames() const {
int num_finite_timestamps =
CountFiniteTimestamps(&FrameT::assembled_timestamp);
RTC_DCHECK_EQ(num_finite_timestamps, self().frames.size());
return num_finite_timestamps;
}
// Samples of per-frame sizes.
SamplesStatsCounter NumPackets() const {
return BuildSamplesPositiveInt(&FrameT::num_packets);
}
SamplesStatsCounter SizeBytes() const {
return BuildSamplesPositiveInt(
[](const auto& frame) { return frame.size.bytes(); });
}
// Samples of per-frame delay variation metrics.
SamplesStatsCounter FrameDelayVariationMs() const {
return BuildSamplesMs(&FrameT::frame_delay_variation);
}
SamplesStatsCounter InterDepartureTimeMs() {
SortByDepartureOrder(self().frames);
return BuildSamplesMs(&InterDepartureTime<FrameT>);
}
SamplesStatsCounter InterArrivalTimeMs() {
SortByArrivalOrder(self().frames);
return BuildSamplesMs(&InterArrivalTime<FrameT>);
}
SamplesStatsCounter InterFrameDelayVariationMs() {
SortByArrivalOrder(self().frames);
return BuildSamplesMs(&InterFrameDelayVariation<FrameT>);
}
SamplesStatsCounter InterAssembledTimeMs() {
SortByAssembledOrder(self().frames);
return BuildSamplesMs(&InterAssembledTime<FrameT>);
}
};
// -- Comparators and sorting --
template <typename StreamT>
bool StreamOrder(const StreamT& a, const StreamT& b) {
if (a.creation_timestamp != b.creation_timestamp) {
return a.creation_timestamp < b.creation_timestamp;
}
return a.ssrc < b.ssrc;
}
template <typename StreamT>
void SortByStreamOrder(std::vector<StreamT>& streams) {
absl::c_stable_sort(streams, StreamOrder<StreamT>);
}
} // namespace webrtc::video_timing_simulator
#endif // VIDEO_TIMING_SIMULATOR_STREAM_BASE_H_