| /* | |
| * 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_ |