Video timing simulator: Improve frame and stream metrics
This change introduces `FrameBase` and `StreamBase`, that are base
classes for the corresponding derived frame and stream classes for
decodability and rendering simulation. Their purpose is to share value
accessors for RTP-level information, while retaining the data members in
the derived structs.
Additionally, we restructure the inter-frame metrics a bit, add
compararators and sorting, and add some more jitter buffer metrics.
Bug: b/423646186
Change-Id: Ida87c44b05e94420dbf218b4299cec97addfce64
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/443220
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Commit-Queue: Rasmus Brandt <brandtr@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#46724}
diff --git a/video/timing/simulator/BUILD.gn b/video/timing/simulator/BUILD.gn
index 113da9e..810abe4 100644
--- a/video/timing/simulator/BUILD.gn
+++ b/video/timing/simulator/BUILD.gn
@@ -17,6 +17,7 @@
"decodability_simulator.h",
"decodability_tracker.cc",
"decodability_tracker.h",
+ "frame_base.h",
"rendering_simulator.cc",
"rendering_simulator.h",
"rendering_tracker.cc",
@@ -25,6 +26,7 @@
"rtc_event_log_driver.h",
"rtp_packet_simulator.cc",
"rtp_packet_simulator.h",
+ "stream_base.h",
]
deps = [
"../../../api:array_view",
@@ -37,6 +39,7 @@
"../../../api:transport_api",
"../../../api/environment",
"../../../api/environment:environment_factory",
+ "../../../api/numerics",
"../../../api/task_queue",
"../../../api/task_queue:pending_task_safety_flag",
"../../../api/units:data_size",
@@ -94,10 +97,12 @@
"assembler_unittest.cc",
"decodability_simulator_unittest.cc",
"decodability_tracker_unittest.cc",
+ "frame_base_unittest.cc",
"rendering_simulator_unittest.cc",
"rendering_tracker_unittest.cc",
"rtc_event_log_driver_unittest.cc",
"rtp_packet_simulator_unittest.cc",
+ "stream_base_unittest.cc",
]
deps = [
":simulator",
diff --git a/video/timing/simulator/decodability_simulator.cc b/video/timing/simulator/decodability_simulator.cc
index 4f7b98f..06b4dd6 100644
--- a/video/timing/simulator/decodability_simulator.cc
+++ b/video/timing/simulator/decodability_simulator.cc
@@ -15,7 +15,6 @@
#include <optional>
#include <utility>
-#include "absl/algorithm/container.h"
#include "absl/base/nullability.h"
#include "absl/container/flat_hash_map.h"
#include "api/environment/environment.h"
@@ -31,6 +30,7 @@
#include "rtc_base/thread_annotations.h"
#include "video/timing/simulator/assembler.h"
#include "video/timing/simulator/decodability_tracker.h"
+#include "video/timing/simulator/frame_base.h"
#include "video/timing/simulator/rtc_event_log_driver.h"
namespace webrtc::video_timing_simulator {
@@ -103,7 +103,7 @@
for (const auto& [key, value] : frames_) {
stream.frames.push_back(value);
}
- absl::c_sort(stream.frames);
+ SortByArrivalOrder(stream.frames);
return stream;
}
@@ -184,7 +184,7 @@
rtc_event_log_simulator.Simulate();
// Return.
- absl::c_sort(results.streams);
+ SortByStreamOrder(results.streams);
return results;
}
diff --git a/video/timing/simulator/decodability_simulator.h b/video/timing/simulator/decodability_simulator.h
index adb8d90..527a750 100644
--- a/video/timing/simulator/decodability_simulator.h
+++ b/video/timing/simulator/decodability_simulator.h
@@ -12,13 +12,16 @@
#define VIDEO_TIMING_SIMULATOR_DECODABILITY_SIMULATOR_H_
#include <cstdint>
-#include <optional>
#include <vector>
+#include "absl/algorithm/container.h"
+#include "api/array_view.h"
#include "api/units/data_size.h"
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "logging/rtc_event_log/rtc_event_log_parser.h"
+#include "video/timing/simulator/frame_base.h"
+#include "video/timing/simulator/stream_base.h"
namespace webrtc::video_timing_simulator {
@@ -34,7 +37,8 @@
};
// Metadata about a single decodable frame.
- struct Frame {
+ struct Frame : public FrameBase<Frame> {
+ // -- Values --
// Frame information.
int num_packets = -1;
DataSize size = DataSize::Zero();
@@ -46,54 +50,25 @@
Timestamp assembled_timestamp = Timestamp::PlusInfinity();
Timestamp decodable_timestamp = Timestamp::PlusInfinity();
- bool operator<(const Frame& other) const {
- return decodable_timestamp < other.decodable_timestamp;
- }
+ // -- Populated values --
+ // One-way delay relative some baseline.
+ TimeDelta frame_delay_variation = TimeDelta::PlusInfinity();
- std::optional<int> InterPacketCount(const Frame& prev) const {
- if (num_packets <= 0 || prev.num_packets <= 0) {
- return std::nullopt;
- }
- return num_packets - prev.num_packets;
- }
- std::optional<int64_t> InterFrameSizeBytes(const Frame& prev) const {
- if (size.IsZero() || prev.size.IsZero()) {
- return std::nullopt;
- }
- return size.bytes() - prev.size.bytes();
- }
- TimeDelta InterDepartureTime(const Frame& prev) const {
- if (unwrapped_rtp_timestamp < 0 || prev.unwrapped_rtp_timestamp < 0) {
- return TimeDelta::PlusInfinity();
- }
- constexpr int64_t kRtpTicksPerMs = 90;
- int64_t inter_departure_time_ms =
- (unwrapped_rtp_timestamp - prev.unwrapped_rtp_timestamp) /
- kRtpTicksPerMs;
- return TimeDelta::Millis(inter_departure_time_ms);
- }
- TimeDelta InterAssemblyTime(const Frame& prev) const {
- return assembled_timestamp - prev.assembled_timestamp;
- }
- TimeDelta InterArrivalTime(const Frame& prev) const {
- return decodable_timestamp - prev.decodable_timestamp;
+ // -- Value accessors --
+ Timestamp ArrivalTimestampInternal() const { return decodable_timestamp; }
+
+ // -- Per-frame metrics --
+ // Time spent waiting for reference frames to arrive.
+ TimeDelta UndecodableDuration() const {
+ return decodable_timestamp - assembled_timestamp;
}
};
// All frames in one stream.
- struct Stream {
+ struct Stream : public StreamBase<Stream> {
Timestamp creation_timestamp = Timestamp::PlusInfinity();
uint32_t ssrc = 0;
std::vector<Frame> frames;
-
- bool IsEmpty() const { return frames.empty(); }
-
- bool operator<(const Stream& other) const {
- if (creation_timestamp != other.creation_timestamp) {
- return creation_timestamp < other.creation_timestamp;
- }
- return ssrc < other.ssrc;
- }
};
// All streams.
@@ -113,6 +88,23 @@
const Config config_;
};
+// -- Comparators and sorting --
+inline bool DecodableOrder(const DecodabilitySimulator::Frame& a,
+ const DecodabilitySimulator::Frame& b) {
+ return a.decodable_timestamp < b.decodable_timestamp;
+}
+inline void SortByDecodableOrder(
+ ArrayView<DecodabilitySimulator::Frame> frames) {
+ absl::c_stable_sort(frames, DecodableOrder);
+}
+
+// -- Inter-frame metrics --
+// Difference in decodable time between two frames.
+inline TimeDelta InterDecodableTime(const DecodabilitySimulator::Frame& cur,
+ const DecodabilitySimulator::Frame& prev) {
+ return cur.decodable_timestamp - prev.decodable_timestamp;
+}
+
} // namespace webrtc::video_timing_simulator
#endif // VIDEO_TIMING_SIMULATOR_DECODABILITY_SIMULATOR_H_
diff --git a/video/timing/simulator/frame_base.h b/video/timing/simulator/frame_base.h
new file mode 100644
index 0000000..f1610df
--- /dev/null
+++ b/video/timing/simulator/frame_base.h
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+#ifndef VIDEO_TIMING_SIMULATOR_FRAME_BASE_H_
+#define VIDEO_TIMING_SIMULATOR_FRAME_BASE_H_
+
+#include <cstdint>
+#include <optional>
+#include <vector>
+
+#include "absl/algorithm/container.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+
+namespace webrtc::video_timing_simulator {
+
+// CRTP base struct for code reuse of departure and arrival timestamp functions.
+template <typename FrameT>
+struct FrameBase {
+ // Data members are defined in derived struct.
+
+ // -- CRTP accessor --
+ const FrameT& self() const { return static_cast<const FrameT&>(*this); }
+
+ // -- Value accessors --
+
+ // Departure time (possibly offset), as determined by RTP timestamp from
+ // the derived class.
+ Timestamp DepartureTimestamp(Timestamp offset = Timestamp::Zero()) const {
+ int64_t unwrapped_rtp_timestamp = self().unwrapped_rtp_timestamp;
+ if (unwrapped_rtp_timestamp < 0) {
+ return Timestamp::PlusInfinity();
+ }
+ constexpr int64_t kMicrosPerMillis = 1'000;
+ constexpr int64_t kRtpVideoTicksPerMillis = 90;
+ // Convert from RTP ticks to microseconds using integer division with
+ // truncation. Note that this introduces an error of up to 1us. That is fine
+ // for our purposes however: the arrival timestamp is logged in ms and the
+ // expected frame delay variation caused by the network is also on the order
+ // of ms.
+ int64_t departure_timestamp_us =
+ (unwrapped_rtp_timestamp * kMicrosPerMillis) / kRtpVideoTicksPerMillis;
+ return Timestamp::Micros(departure_timestamp_us - offset.us());
+ }
+
+ // Arrival time (possibly offset), as determined by
+ // `ArrivalTimestampInternal()` from the derived class. This allows derived
+ // classes to define themselves the meaning of "arrival": typically decodable
+ // or rendered, but could be assembled or decoded as well.
+ Timestamp ArrivalTimestamp(Timestamp offset = Timestamp::Zero()) const {
+ Timestamp arrival_timestamp = self().ArrivalTimestampInternal();
+ if (!arrival_timestamp.IsFinite()) {
+ return arrival_timestamp;
+ }
+ return Timestamp::Micros(arrival_timestamp.us() - offset.us());
+ }
+
+ // -- Per-frame metrics --
+ // One way delay with required timestamp offset normalization.
+ TimeDelta OneWayDelay(Timestamp arrival_offset,
+ Timestamp departure_offset) const {
+ return ArrivalTimestamp(arrival_offset) -
+ DepartureTimestamp(departure_offset);
+ }
+};
+
+// -- Comparators and sorting --
+template <typename FrameT>
+bool DepartureOrder(const FrameT& a, const FrameT& b) {
+ return a.DepartureTimestamp() < b.DepartureTimestamp();
+}
+template <typename FrameT>
+void SortByDepartureOrder(std::vector<FrameT>& frames) {
+ absl::c_stable_sort(frames, DepartureOrder<FrameT>);
+}
+
+template <typename FrameT>
+bool ArrivalOrder(const FrameT& a, const FrameT& b) {
+ return a.ArrivalTimestamp() < b.ArrivalTimestamp();
+}
+template <typename FrameT>
+void SortByArrivalOrder(std::vector<FrameT>& frames) {
+ absl::c_stable_sort(frames, ArrivalOrder<FrameT>);
+}
+
+template <typename FrameT>
+inline bool AssembledOrder(const FrameT& a, const FrameT& b) {
+ return a.assembled_timestamp < b.assembled_timestamp;
+}
+template <typename FrameT>
+inline void SortByAssembledOrder(std::vector<FrameT> frames) {
+ absl::c_stable_sort(frames, AssembledOrder<FrameT>);
+}
+
+// --- Inter-frame metrics ---
+// Difference in packet counts between two frames.
+template <typename FrameT>
+std::optional<int> InterPacketCount(const FrameT& cur, const FrameT& prev) {
+ if (cur.num_packets <= 0 || prev.num_packets <= 0) {
+ return std::nullopt;
+ }
+ return cur.num_packets - prev.num_packets;
+}
+
+// Difference in frame size (bytes) between two frames.
+template <typename FrameT>
+std::optional<int64_t> InterFrameSizeBytes(const FrameT& cur,
+ const FrameT& prev) {
+ if (cur.size.IsZero() || prev.size.IsZero()) {
+ return std::nullopt;
+ }
+ return cur.size.bytes() - prev.size.bytes();
+}
+
+// Difference in departure timestamp between two frames.
+template <typename FrameT>
+TimeDelta InterDepartureTime(const FrameT& cur, const FrameT& prev) {
+ return cur.DepartureTimestamp() - prev.DepartureTimestamp();
+}
+
+// Difference in arrival timestamp between two frames.
+template <typename FrameT>
+TimeDelta InterArrivalTime(const FrameT& cur, const FrameT& prev) {
+ return cur.ArrivalTimestamp() - prev.ArrivalTimestamp();
+}
+
+// https://datatracker.ietf.org/doc/html/rfc5481#section-1
+template <typename FrameT>
+TimeDelta InterFrameDelayVariation(const FrameT& cur, const FrameT& prev) {
+ return InterArrivalTime(cur, prev) - InterDepartureTime(cur, prev);
+}
+
+// Difference in assembled timestamp between two frames.
+template <typename FrameT>
+TimeDelta InterAssembledTime(const FrameT& cur, const FrameT& prev) {
+ return cur.assembled_timestamp - prev.assembled_timestamp;
+}
+
+} // namespace webrtc::video_timing_simulator
+
+#endif // VIDEO_TIMING_SIMULATOR_FRAME_BASE_H_
diff --git a/video/timing/simulator/frame_base_unittest.cc b/video/timing/simulator/frame_base_unittest.cc
new file mode 100644
index 0000000..63829f8
--- /dev/null
+++ b/video/timing/simulator/frame_base_unittest.cc
@@ -0,0 +1,117 @@
+/*
+ * 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 "video/timing/simulator/frame_base.h"
+
+#include <cstdint>
+#include <vector>
+
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "test/gtest.h"
+
+namespace webrtc::video_timing_simulator {
+namespace {
+
+constexpr int64_t kMicrosPerMillis = 1000;
+constexpr int64_t kRtpVideoTicksPerMillis = 90;
+
+struct TestFrame : public FrameBase<TestFrame> {
+ int64_t unwrapped_rtp_timestamp = -1;
+ Timestamp assembled_timestamp = Timestamp::PlusInfinity();
+ Timestamp ArrivalTimestampInternal() const { return assembled_timestamp; }
+};
+
+TEST(FrameBaseTest, DepartureTimestampIsInvalidForUnsetRtpTimestamp) {
+ TestFrame frame;
+ EXPECT_FALSE(frame.DepartureTimestamp().IsFinite());
+}
+
+TEST(FrameBaseTest, DepartureTimestamp) {
+ TestFrame frame{.unwrapped_rtp_timestamp = 3000};
+ EXPECT_EQ(frame.DepartureTimestamp(), Timestamp::Micros(33333));
+}
+
+TEST(FrameBaseTest, DepartureTimestampWithOffset) {
+ int64_t rtp_timestamp_offset = 123456789;
+ Timestamp departure_timestamp_offset = Timestamp::Micros(
+ (rtp_timestamp_offset * kMicrosPerMillis) / kRtpVideoTicksPerMillis);
+ TestFrame frame{.unwrapped_rtp_timestamp = rtp_timestamp_offset + 3000};
+ EXPECT_EQ(frame.DepartureTimestamp(departure_timestamp_offset),
+ Timestamp::Micros(33333));
+}
+
+TEST(FrameBaseTest, ArrivalTimestampIsInvalidForUnsetRtpTimestamp) {
+ TestFrame frame;
+ EXPECT_FALSE(frame.ArrivalTimestamp().IsFinite());
+}
+
+TEST(FrameBaseTest, ArrivalTimestamp) {
+ TestFrame frame{.assembled_timestamp = Timestamp::Micros(33333)};
+ EXPECT_EQ(frame.ArrivalTimestamp(), Timestamp::Micros(33333));
+}
+
+TEST(FrameBaseTest, ArrivalTimestampWithOffset) {
+ Timestamp arrival_timestamp_offset = Timestamp::Seconds(123456789);
+ TestFrame frame{.assembled_timestamp =
+ Timestamp::Micros(arrival_timestamp_offset.us() + 33333)};
+ EXPECT_EQ(frame.ArrivalTimestamp(arrival_timestamp_offset),
+ Timestamp::Micros(33333));
+}
+
+TEST(FrameBaseTest, OneWayDelayWithZeroOffsets) {
+ TestFrame frame1{.unwrapped_rtp_timestamp = 3000,
+ .assembled_timestamp = Timestamp::Micros(33333)};
+ EXPECT_EQ(frame1.OneWayDelay(
+ /*arrival_offset=*/Timestamp::Zero(),
+ /*departure_offset=*/Timestamp::Zero()),
+ TimeDelta::Zero());
+
+ // Delayed 1000us relative to its nominal arrival time.
+ TestFrame frame2{.unwrapped_rtp_timestamp = 6000,
+ .assembled_timestamp = Timestamp::Micros(67666)};
+ EXPECT_EQ(frame2.OneWayDelay(
+ /*arrival_offset=*/Timestamp::Zero(),
+ /*departure_offset=*/Timestamp::Zero()),
+ TimeDelta::Micros(1000));
+}
+
+TEST(FrameBaseTest, OneWayDelayWithOffsets) {
+ int64_t rtp_timestamp_offset = 123456789;
+ Timestamp departure_timestamp_offset = Timestamp::Micros(
+ (rtp_timestamp_offset * kMicrosPerMillis) / kRtpVideoTicksPerMillis);
+ Timestamp arrival_timestamp_offset = Timestamp::Seconds(123456789);
+
+ TestFrame frame1{.unwrapped_rtp_timestamp = rtp_timestamp_offset + 3000,
+ .assembled_timestamp = Timestamp::Micros(
+ arrival_timestamp_offset.us() + 33333)};
+ EXPECT_EQ(
+ frame1.OneWayDelay(arrival_timestamp_offset, departure_timestamp_offset),
+ TimeDelta::Zero());
+
+ // Delayed 1000us relative to its nominal arrival time.
+ TestFrame frame2{.unwrapped_rtp_timestamp = rtp_timestamp_offset + 6000,
+ .assembled_timestamp = Timestamp::Micros(
+ arrival_timestamp_offset.us() + 67666)};
+ EXPECT_EQ(
+ frame2.OneWayDelay(arrival_timestamp_offset, departure_timestamp_offset),
+ TimeDelta::Micros(1000));
+}
+
+TEST(FrameBaseTest, SortingTemplatesCompile) {
+ std::vector<TestFrame> test_frames = {
+ TestFrame{.unwrapped_rtp_timestamp = 3000}};
+ SortByDepartureOrder(test_frames);
+ SortByArrivalOrder(test_frames);
+ SortByAssembledOrder(test_frames);
+}
+
+} // namespace
+} // namespace webrtc::video_timing_simulator
diff --git a/video/timing/simulator/rendering_simulator.cc b/video/timing/simulator/rendering_simulator.cc
index 71ba065..1293c8d 100644
--- a/video/timing/simulator/rendering_simulator.cc
+++ b/video/timing/simulator/rendering_simulator.cc
@@ -16,7 +16,6 @@
#include <optional>
#include <utility>
-#include "absl/algorithm/container.h"
#include "absl/base/nullability.h"
#include "absl/container/flat_hash_map.h"
#include "api/environment/environment.h"
@@ -33,6 +32,7 @@
#include "rtc_base/numerics/sequence_number_unwrapper.h"
#include "rtc_base/thread_annotations.h"
#include "video/timing/simulator/assembler.h"
+#include "video/timing/simulator/frame_base.h"
#include "video/timing/simulator/rendering_tracker.h"
#include "video/timing/simulator/rtc_event_log_driver.h"
@@ -151,7 +151,7 @@
RTC_DCHECK_EQ(key, value.frame_id);
stream.frames.push_back(value);
}
- absl::c_sort(stream.frames);
+ SortByArrivalOrder(stream.frames);
return stream;
}
@@ -240,7 +240,7 @@
rtc_event_log_simulator.Simulate();
// Return.
- absl::c_sort(results.streams);
+ SortByStreamOrder(results.streams);
return results;
}
diff --git a/video/timing/simulator/rendering_simulator.h b/video/timing/simulator/rendering_simulator.h
index 439509f..8e74cbf 100644
--- a/video/timing/simulator/rendering_simulator.h
+++ b/video/timing/simulator/rendering_simulator.h
@@ -18,12 +18,16 @@
#include <string>
#include <vector>
+#include "absl/algorithm/container.h"
+#include "api/array_view.h"
#include "api/environment/environment.h"
#include "api/units/data_size.h"
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "logging/rtc_event_log/rtc_event_log_parser.h"
#include "modules/video_coding/timing/timing.h"
+#include "video/timing/simulator/frame_base.h"
+#include "video/timing/simulator/stream_base.h"
namespace webrtc::video_timing_simulator {
@@ -48,7 +52,8 @@
};
// Metadata about a single rendered frame.
- struct Frame {
+ struct Frame : public FrameBase<Frame> {
+ // -- Values --
// Frame information.
int num_packets = -1;
DataSize size = DataSize::Zero();
@@ -77,59 +82,83 @@
// Jitter buffer state at the time of this frame.
int frames_dropped = -1;
// TODO: b/423646186 - Add `current_delay_ms`.
+ // The `jitter_buffer_*` metrics below are recorded by the production code,
+ // and should be compatible with the `webrtc-stats` definitions. One major
+ // difference is that they are _not_ cumulative.
+ // https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferminimumdelay
TimeDelta jitter_buffer_minimum_delay = TimeDelta::MinusInfinity();
+ // https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbuffertargetdelay
TimeDelta jitter_buffer_target_delay = TimeDelta::MinusInfinity();
+ // https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferdelay
TimeDelta jitter_buffer_delay = TimeDelta::MinusInfinity();
- bool operator<(const Frame& other) const {
- return rendered_timestamp < other.rendered_timestamp;
- }
+ // -- Populated values --
+ // One-way delay relative some baseline.
+ TimeDelta frame_delay_variation = TimeDelta::PlusInfinity();
- std::optional<int64_t> InterFrameSizeBytes(const Frame& prev) const {
- if (size.IsZero() || prev.size.IsZero()) {
- return std::nullopt;
- }
- return size.bytes() - prev.size.bytes();
+ // -- Value accessors --
+ Timestamp ArrivalTimestampInternal() const { return rendered_timestamp; }
+
+ // -- Per-frame metrics --
+ // Time spent being assembled (waiting for all packets to arrive).
+ TimeDelta PacketBufferDuration() const {
+ return assembled_timestamp - first_packet_arrival_timestamp;
}
- TimeDelta InterDepartureTime(const Frame& prev) const {
- if (unwrapped_rtp_timestamp < 0 || prev.unwrapped_rtp_timestamp < 0) {
- return TimeDelta::PlusInfinity();
- }
- constexpr int64_t kRtpTicksPerMs = 90;
- int64_t inter_departure_time_ms =
- (unwrapped_rtp_timestamp - prev.unwrapped_rtp_timestamp) /
- kRtpTicksPerMs;
- return TimeDelta::Millis(inter_departure_time_ms);
- }
- TimeDelta InterArrivalTime(const Frame& prev) const {
- return rendered_timestamp - prev.rendered_timestamp;
- }
- TimeDelta AssemblyDuration() const {
- return last_packet_arrival_timestamp - first_packet_arrival_timestamp;
- }
- TimeDelta PreDecodeBufferDuration() const {
+ // Time spent waiting to be decoded, after assembly. (This includes a,
+ // currently, zero decode duration.) Note that this is similar to
+ // `jitter_buffer_delay`, except that the latter is 1) recorded by the
+ // production code; and, 2) is anchored on `first_packet_arrival_timestamp`
+ // rather than `assembled_timestamp`.
+ TimeDelta FrameBufferDuration() const {
return decoded_timestamp - assembled_timestamp;
}
- TimeDelta PostDecodeMargin() const {
- return render_timestamp - rendered_timestamp -
- RenderingSimulator::kRenderDelay;
+ // Time spent waiting to be rendered, after decode.
+ TimeDelta RenderBufferDuration() const {
+ return rendered_timestamp - decoded_timestamp;
+ }
+ // Margin between render timestamp (target) and
+ // assembled timestamp (actual):
+ // * A frame that is assembled early w.r.t. the target (<=> arriving
+ // on time from the network) has a positive margin.
+ // * A frame that is assembled on time w.r.t. the target (<=> arriving
+ // slightly late from the network) has zero margin.
+ // * A frame that is assembled late w.r.t. the target (<=> arriving
+ // very late from the network) has negative margin.
+ // Positive margins mean no video freezes, at the cost of receiver delay.
+ // A jitter buffer needs to strike a balance between video freezes and
+ // delay. In terms of margin, that means low positive margin values, a
+ // couple of frames with zero margin, and very few frames with
+ // negative margin.
+ TimeDelta RenderMargin() const {
+ return render_timestamp - assembled_timestamp;
+ }
+ // Split the margin along zero: "excess margin" for positive margins and
+ // "deficit margin" for negative margins.
+ // A jitter buffer would generally want to minimize the number of frames
+ // with a deficit margin (delayed frames/buffer underruns => video freezes),
+ // while also minimizing the stream-level min/p10 of the excess margin
+ // (early frames spending a long time in the buffer => high latency).
+ std::optional<TimeDelta> RenderedExcessMargin() const {
+ TimeDelta margin = RenderMargin();
+ if (margin < TimeDelta::Zero()) {
+ return std::nullopt;
+ }
+ return margin;
+ }
+ std::optional<TimeDelta> RenderedDeficitMargin() const {
+ TimeDelta margin = RenderMargin();
+ if (margin > TimeDelta::Zero()) {
+ return std::nullopt;
+ }
+ return margin;
}
};
// All frames in one stream.
- struct Stream {
+ struct Stream : public StreamBase<Stream> {
Timestamp creation_timestamp = Timestamp::PlusInfinity();
uint32_t ssrc = 0;
std::vector<Frame> frames;
-
- bool IsEmpty() const { return frames.empty(); }
-
- bool operator<(const Stream& other) const {
- if (creation_timestamp != other.creation_timestamp) {
- return creation_timestamp < other.creation_timestamp;
- }
- return ssrc < other.ssrc;
- }
};
// All streams.
@@ -153,6 +182,50 @@
const Config config_;
};
+// -- Comparators and sorting --
+inline bool RenderOrder(const RenderingSimulator::Frame& a,
+ const RenderingSimulator::Frame& b) {
+ return a.render_timestamp < b.render_timestamp;
+}
+inline void SortByRenderOrder(ArrayView<RenderingSimulator::Frame> frames) {
+ absl::c_stable_sort(frames, RenderOrder);
+}
+
+inline bool DecodedOrder(const RenderingSimulator::Frame& a,
+ const RenderingSimulator::Frame& b) {
+ return a.decoded_timestamp < b.decoded_timestamp;
+}
+inline void SortByDecodedOrder(ArrayView<RenderingSimulator::Frame> frames) {
+ absl::c_stable_sort(frames, DecodedOrder);
+}
+
+inline bool RenderedOrder(const RenderingSimulator::Frame& a,
+ const RenderingSimulator::Frame& b) {
+ return a.rendered_timestamp < b.rendered_timestamp;
+}
+inline void SortByRenderedOrder(ArrayView<RenderingSimulator::Frame> frames) {
+ absl::c_stable_sort(frames, RenderedOrder);
+}
+
+// -- Inter-frame metrics --
+// Difference in render time (target) between two frames.
+inline TimeDelta InterRenderTime(const RenderingSimulator::Frame& cur,
+ const RenderingSimulator::Frame& prev) {
+ return cur.render_timestamp - prev.render_timestamp;
+}
+
+// Difference in decoded time (actual) between two frames.
+inline TimeDelta InterDecodedTime(const RenderingSimulator::Frame& cur,
+ const RenderingSimulator::Frame& prev) {
+ return cur.decoded_timestamp - prev.decoded_timestamp;
+}
+
+// Difference in rendered time (actual) between two frames.
+inline TimeDelta InterRenderedTime(const RenderingSimulator::Frame& cur,
+ const RenderingSimulator::Frame& prev) {
+ return cur.rendered_timestamp - prev.rendered_timestamp;
+}
+
} // namespace webrtc::video_timing_simulator
#endif // VIDEO_TIMING_SIMULATOR_RENDERING_SIMULATOR_H_
diff --git a/video/timing/simulator/stream_base.h b/video/timing/simulator/stream_base.h
new file mode 100644
index 0000000..a097188
--- /dev/null
+++ b/video/timing/simulator/stream_base.h
@@ -0,0 +1,95 @@
+/*
+ * 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 <cstddef>
+#include <vector>
+
+#include "absl/algorithm/container.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/numerics/moving_percentile_filter.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>
+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(); }
+
+ // -- Metric population --
+ void PopulateFrameDelayVariations(float baseline_percentile = 0.0,
+ size_t baseline_window_size = 300) {
+ auto& frames = self().frames;
+ if (frames.empty()) {
+ 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);
+
+ // One-way delay measurement offsets.
+ Timestamp arrival_offset = Timestamp::PlusInfinity();
+ Timestamp departure_offset = Timestamp::PlusInfinity();
+ for (const auto& frame : 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;
+ }
+
+ // Calculate frame delay variations relative the moving baseline.
+ for (auto& frame : 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();
+ }
+ }
+};
+
+// -- 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_
diff --git a/video/timing/simulator/stream_base_unittest.cc b/video/timing/simulator/stream_base_unittest.cc
new file mode 100644
index 0000000..ff1ec4d
--- /dev/null
+++ b/video/timing/simulator/stream_base_unittest.cc
@@ -0,0 +1,81 @@
+/*
+ * 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 "video/timing/simulator/stream_base.h"
+
+#include <cstdint>
+#include <vector>
+
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "video/timing/simulator/frame_base.h"
+
+namespace webrtc::video_timing_simulator {
+namespace {
+
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::Field;
+
+struct TestFrame : public FrameBase<TestFrame> {
+ int64_t unwrapped_rtp_timestamp = -1;
+ Timestamp assembled_timestamp = Timestamp::PlusInfinity();
+ Timestamp ArrivalTimestampInternal() const { return assembled_timestamp; }
+ TimeDelta frame_delay_variation = TimeDelta::PlusInfinity();
+};
+
+struct TestStream : public StreamBase<TestStream> {
+ Timestamp creation_timestamp = Timestamp::PlusInfinity();
+ uint32_t ssrc = 0;
+ std::vector<TestFrame> frames;
+};
+
+TEST(StreamBaseTest, IsEmpty) {
+ TestStream stream;
+ EXPECT_TRUE(stream.IsEmpty());
+}
+
+TEST(StreamBaseTest, PopulateFrameDelayVariations) {
+ // Four frames at 30fps => 3000 RTP ticks between sent frames.
+ // Nominal inter-arrival-time is 33333us.
+
+ // First frame becomes the initial baseline.
+ TestFrame frame1{.unwrapped_rtp_timestamp = 3000,
+ .assembled_timestamp = Timestamp::Micros(33333)};
+ // Second frame is delayed 1000us.
+ TestFrame frame2{.unwrapped_rtp_timestamp = 6000,
+ .assembled_timestamp = Timestamp::Micros(66666 + 1000)};
+ // Third frame is severely delayed, arriving back-to-back with the 4th frame.
+ TestFrame frame3{.unwrapped_rtp_timestamp = 9000,
+ .assembled_timestamp = Timestamp::Micros(99999 + 33333)};
+ // The 4th frame arrives on time.
+ TestFrame frame4{.unwrapped_rtp_timestamp = 12000,
+ .assembled_timestamp = Timestamp::Micros(133332)};
+
+ TestStream stream{.frames = {frame1, frame2, frame3, frame4}};
+ stream.PopulateFrameDelayVariations();
+
+ EXPECT_THAT(
+ stream.frames,
+ ElementsAreArray({
+ Field(&TestFrame::frame_delay_variation, Eq(TimeDelta::Zero())),
+ Field(&TestFrame::frame_delay_variation, Eq(TimeDelta::Micros(1000))),
+ // Due to the non-integer 1000/90 factor in the timestamp
+ // translation, we get a 33332us here instead of 33333us.
+ Field(&TestFrame::frame_delay_variation,
+ Eq(TimeDelta::Micros(33332))),
+ Field(&TestFrame::frame_delay_variation, Eq(TimeDelta::Zero())),
+ }));
+}
+
+} // namespace
+} // namespace webrtc::video_timing_simulator