summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc')
-rw-r--r--third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc1101
1 files changed, 1101 insertions, 0 deletions
diff --git a/third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc b/third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc
new file mode 100644
index 0000000000..afc675ffde
--- /dev/null
+++ b/third_party/libwebrtc/video/frame_cadence_adapter_unittest.cc
@@ -0,0 +1,1101 @@
+/*
+ * Copyright (c) 2021 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/frame_cadence_adapter.h"
+
+#include <utility>
+#include <vector>
+
+#include "absl/functional/any_invocable.h"
+#include "api/task_queue/default_task_queue_factory.h"
+#include "api/task_queue/task_queue_base.h"
+#include "api/task_queue/task_queue_factory.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "api/video/nv12_buffer.h"
+#include "api/video/video_frame.h"
+#include "rtc_base/event.h"
+#include "rtc_base/rate_statistics.h"
+#include "rtc_base/time_utils.h"
+#include "system_wrappers/include/metrics.h"
+#include "system_wrappers/include/ntp_time.h"
+#include "system_wrappers/include/sleep.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "test/scoped_key_value_config.h"
+#include "test/time_controller/simulated_time_controller.h"
+
+namespace webrtc {
+namespace {
+
+using ::testing::_;
+using ::testing::ElementsAre;
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::Pair;
+using ::testing::Values;
+
+VideoFrame CreateFrame() {
+ return VideoFrame::Builder()
+ .set_video_frame_buffer(
+ rtc::make_ref_counted<NV12Buffer>(/*width=*/16, /*height=*/16))
+ .build();
+}
+
+VideoFrame CreateFrameWithTimestamps(
+ GlobalSimulatedTimeController* time_controller) {
+ return VideoFrame::Builder()
+ .set_video_frame_buffer(
+ rtc::make_ref_counted<NV12Buffer>(/*width=*/16, /*height=*/16))
+ .set_ntp_time_ms(time_controller->GetClock()->CurrentNtpInMilliseconds())
+ .set_timestamp_us(time_controller->GetClock()->CurrentTime().us())
+ .build();
+}
+
+std::unique_ptr<FrameCadenceAdapterInterface> CreateAdapter(
+ const FieldTrialsView& field_trials,
+ Clock* clock) {
+ return FrameCadenceAdapterInterface::Create(clock, TaskQueueBase::Current(),
+ field_trials);
+}
+
+class MockCallback : public FrameCadenceAdapterInterface::Callback {
+ public:
+ MOCK_METHOD(void, OnFrame, (Timestamp, int, const VideoFrame&), (override));
+ MOCK_METHOD(void, OnDiscardedFrame, (), (override));
+ MOCK_METHOD(void, RequestRefreshFrame, (), (override));
+};
+
+class ZeroHertzFieldTrialDisabler : public test::ScopedKeyValueConfig {
+ public:
+ ZeroHertzFieldTrialDisabler()
+ : test::ScopedKeyValueConfig("WebRTC-ZeroHertzScreenshare/Disabled/") {}
+};
+
+class ZeroHertzFieldTrialEnabler : public test::ScopedKeyValueConfig {
+ public:
+ ZeroHertzFieldTrialEnabler()
+ : test::ScopedKeyValueConfig("WebRTC-ZeroHertzScreenshare/Enabled/") {}
+};
+
+TEST(FrameCadenceAdapterTest,
+ ForwardsFramesOnConstructionAndUnderDisabledFieldTrial) {
+ GlobalSimulatedTimeController time_controller(Timestamp::Millis(1));
+ ZeroHertzFieldTrialDisabler disabled_field_trials;
+ test::ScopedKeyValueConfig no_field_trials;
+ for (int i = 0; i != 2; i++) {
+ MockCallback callback;
+ auto adapter =
+ CreateAdapter(i == 0 ? disabled_field_trials : no_field_trials,
+ time_controller.GetClock());
+ adapter->Initialize(&callback);
+ VideoFrame frame = CreateFrame();
+ EXPECT_CALL(callback, OnFrame).Times(1);
+ adapter->OnFrame(frame);
+ time_controller.AdvanceTime(TimeDelta::Zero());
+ Mock::VerifyAndClearExpectations(&callback);
+ EXPECT_CALL(callback, OnDiscardedFrame).Times(1);
+ adapter->OnDiscardedFrame();
+ Mock::VerifyAndClearExpectations(&callback);
+ }
+}
+
+TEST(FrameCadenceAdapterTest, CountsOutstandingFramesToProcess) {
+ test::ScopedKeyValueConfig no_field_trials;
+ GlobalSimulatedTimeController time_controller(Timestamp::Millis(1));
+ MockCallback callback;
+ auto adapter = CreateAdapter(no_field_trials, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ EXPECT_CALL(callback, OnFrame(_, 2, _)).Times(1);
+ EXPECT_CALL(callback, OnFrame(_, 1, _)).Times(1);
+ auto frame = CreateFrame();
+ adapter->OnFrame(frame);
+ adapter->OnFrame(frame);
+ time_controller.AdvanceTime(TimeDelta::Zero());
+ EXPECT_CALL(callback, OnFrame(_, 1, _)).Times(1);
+ adapter->OnFrame(frame);
+ time_controller.AdvanceTime(TimeDelta::Zero());
+}
+
+TEST(FrameCadenceAdapterTest, FrameRateFollowsRateStatisticsByDefault) {
+ test::ScopedKeyValueConfig no_field_trials;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(no_field_trials, time_controller.GetClock());
+ adapter->Initialize(nullptr);
+
+ // Create an "oracle" rate statistics which should be followed on a sequence
+ // of frames.
+ RateStatistics rate(
+ FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+
+ for (int frame = 0; frame != 10; ++frame) {
+ time_controller.AdvanceTime(TimeDelta::Millis(10));
+ rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+ adapter->UpdateFrameRate();
+ EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+ adapter->GetInputFrameRateFps())
+ << " failed for frame " << frame;
+ }
+}
+
+TEST(FrameCadenceAdapterTest,
+ FrameRateFollowsRateStatisticsWhenFeatureDisabled) {
+ ZeroHertzFieldTrialDisabler feature_disabler;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(feature_disabler, time_controller.GetClock());
+ adapter->Initialize(nullptr);
+
+ // Create an "oracle" rate statistics which should be followed on a sequence
+ // of frames.
+ RateStatistics rate(
+ FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+
+ for (int frame = 0; frame != 10; ++frame) {
+ time_controller.AdvanceTime(TimeDelta::Millis(10));
+ rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+ adapter->UpdateFrameRate();
+ EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+ adapter->GetInputFrameRateFps())
+ << " failed for frame " << frame;
+ }
+}
+
+TEST(FrameCadenceAdapterTest, FrameRateFollowsMaxFpsWhenZeroHertzActivated) {
+ ZeroHertzFieldTrialEnabler enabler;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(nullptr);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+ for (int frame = 0; frame != 10; ++frame) {
+ time_controller.AdvanceTime(TimeDelta::Millis(10));
+ adapter->UpdateFrameRate();
+ EXPECT_EQ(adapter->GetInputFrameRateFps(), 1u);
+ }
+}
+
+TEST(FrameCadenceAdapterTest,
+ FrameRateFollowsRateStatisticsAfterZeroHertzDeactivated) {
+ ZeroHertzFieldTrialEnabler enabler;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(nullptr);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+ RateStatistics rate(
+ FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+ constexpr int MAX = 10;
+ for (int frame = 0; frame != MAX; ++frame) {
+ time_controller.AdvanceTime(TimeDelta::Millis(10));
+ rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+ adapter->UpdateFrameRate();
+ }
+ // Turn off zero hertz on the next-last frame; after the last frame we
+ // should see a value that tracks the rate oracle.
+ adapter->SetZeroHertzModeEnabled(absl::nullopt);
+ // Last frame.
+ time_controller.AdvanceTime(TimeDelta::Millis(10));
+ rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+ adapter->UpdateFrameRate();
+
+ EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+ adapter->GetInputFrameRateFps());
+}
+
+TEST(FrameCadenceAdapterTest, ForwardsFramesDelayed) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+ constexpr int kNumFrames = 3;
+ NtpTime original_ntp_time = time_controller.GetClock()->CurrentNtpTime();
+ auto frame = CreateFrameWithTimestamps(&time_controller);
+ int64_t original_timestamp_us = frame.timestamp_us();
+ for (int index = 0; index != kNumFrames; ++index) {
+ EXPECT_CALL(callback, OnFrame).Times(0);
+ adapter->OnFrame(frame);
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int,
+ const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(),
+ original_timestamp_us + index * rtc::kNumMicrosecsPerSec);
+ EXPECT_EQ(frame.ntp_time_ms(), original_ntp_time.ToMs() +
+ index * rtc::kNumMillisecsPerSec);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ frame = CreateFrameWithTimestamps(&time_controller);
+ }
+}
+
+TEST(FrameCadenceAdapterTest, RepeatsFramesDelayed) {
+ // Logic in the frame cadence adapter avoids modifying frame NTP and render
+ // timestamps if these timestamps looks unset, which is the case when the
+ // clock is initialized running from 0. For this reason we choose the
+ // `time_controller` initialization constant to something arbitrary which is
+ // not 0.
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Millis(47892223));
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+ NtpTime original_ntp_time = time_controller.GetClock()->CurrentNtpTime();
+
+ // Send one frame, expect 2 subsequent repeats.
+ auto frame = CreateFrameWithTimestamps(&time_controller);
+ int64_t original_timestamp_us = frame.timestamp_us();
+ adapter->OnFrame(frame);
+
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int, const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(), original_timestamp_us);
+ EXPECT_EQ(frame.ntp_time_ms(), original_ntp_time.ToMs());
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int, const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(),
+ original_timestamp_us + rtc::kNumMicrosecsPerSec);
+ EXPECT_EQ(frame.ntp_time_ms(),
+ original_ntp_time.ToMs() + rtc::kNumMillisecsPerSec);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int, const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(),
+ original_timestamp_us + 2 * rtc::kNumMicrosecsPerSec);
+ EXPECT_EQ(frame.ntp_time_ms(),
+ original_ntp_time.ToMs() + 2 * rtc::kNumMillisecsPerSec);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+}
+
+TEST(FrameCadenceAdapterTest,
+ RepeatsFramesWithoutTimestampsWithUnsetTimestamps) {
+ // Logic in the frame cadence adapter avoids modifying frame NTP and render
+ // timestamps if these timestamps looks unset, which is the case when the
+ // clock is initialized running from 0. In this test we deliberately don't set
+ // it to zero, but select unset timestamps in the frames (via CreateFrame())
+ // and verify that the timestamp modifying logic doesn't depend on the current
+ // time.
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Millis(4711));
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+
+ // Send one frame, expect a repeat.
+ adapter->OnFrame(CreateFrame());
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int, const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(), 0);
+ EXPECT_EQ(frame.ntp_time_ms(), 0);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp post_time, int, const VideoFrame& frame) {
+ EXPECT_EQ(post_time, time_controller.GetClock()->CurrentTime());
+ EXPECT_EQ(frame.timestamp_us(), 0);
+ EXPECT_EQ(frame.ntp_time_ms(), 0);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+}
+
+TEST(FrameCadenceAdapterTest, StopsRepeatingFramesDelayed) {
+ // At 1s, the initially scheduled frame appears.
+ // At 2s, the repeated initial frame appears.
+ // At 2.5s, we schedule another new frame.
+ // At 3.5s, we receive this frame.
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+ NtpTime original_ntp_time = time_controller.GetClock()->CurrentNtpTime();
+
+ // Send one frame, expect 1 subsequent repeat.
+ adapter->OnFrame(CreateFrameWithTimestamps(&time_controller));
+ EXPECT_CALL(callback, OnFrame).Times(2);
+ time_controller.AdvanceTime(TimeDelta::Seconds(2.5));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // Send the new frame at 2.5s, which should appear after 3.5s.
+ adapter->OnFrame(CreateFrameWithTimestamps(&time_controller));
+ EXPECT_CALL(callback, OnFrame)
+ .WillOnce(Invoke([&](Timestamp, int, const VideoFrame& frame) {
+ EXPECT_EQ(frame.timestamp_us(), 5 * rtc::kNumMicrosecsPerSec / 2);
+ EXPECT_EQ(frame.ntp_time_ms(),
+ original_ntp_time.ToMs() + 5u * rtc::kNumMillisecsPerSec / 2);
+ }));
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+}
+
+TEST(FrameCadenceAdapterTest, RequestsRefreshFrameOnKeyFrameRequestWhenNew) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+ EXPECT_CALL(callback, RequestRefreshFrame);
+ time_controller.AdvanceTime(
+ TimeDelta::Seconds(1) *
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod /
+ kMaxFps);
+ adapter->ProcessKeyFrameRequest();
+}
+
+TEST(FrameCadenceAdapterTest, IgnoresKeyFrameRequestShortlyAfterFrame) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 10});
+ adapter->OnFrame(CreateFrame());
+ time_controller.AdvanceTime(TimeDelta::Zero());
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ adapter->ProcessKeyFrameRequest();
+}
+
+TEST(FrameCadenceAdapterTest, RequestsRefreshFramesUntilArrival) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+
+ // We should see max_fps + 1 -
+ // FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod refresh
+ // frame requests during the one second we wait until we send a single frame,
+ // after which refresh frame requests should cease (we should see no such
+ // requests during a second).
+ EXPECT_CALL(callback, RequestRefreshFrame)
+ .Times(kMaxFps + 1 -
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod);
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+ adapter->OnFrame(CreateFrame());
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+}
+
+TEST(FrameCadenceAdapterTest, RequestsRefreshAfterFrameDrop) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+
+ // Send a frame through to cancel the initial delayed timer waiting for first
+ // frame entry.
+ adapter->OnFrame(CreateFrame());
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // Send a dropped frame indication without any following frames received.
+ // After FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod
+ // frame periods, we should receive a first refresh request.
+ adapter->OnDiscardedFrame();
+ EXPECT_CALL(callback, RequestRefreshFrame);
+ time_controller.AdvanceTime(
+ TimeDelta::Seconds(1) *
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod /
+ kMaxFps);
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // We will now receive a refresh frame request for every frame period.
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(kMaxFps);
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // After a frame is passed the requests will cease.
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ adapter->OnFrame(CreateFrame());
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+}
+
+TEST(FrameCadenceAdapterTest, OmitsRefreshAfterFrameDropWithTimelyFrameEntry) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+
+ // Send a frame through to cancel the initial delayed timer waiting for first
+ // frame entry.
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ adapter->OnFrame(CreateFrame());
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // Send a frame drop indication. No refresh frames should be requested
+ // until FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod
+ // intervals pass. Stop short of this.
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ adapter->OnDiscardedFrame();
+ time_controller.AdvanceTime(
+ TimeDelta::Seconds(1) *
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod /
+ kMaxFps -
+ TimeDelta::Micros(1));
+ Mock::VerifyAndClearExpectations(&callback);
+
+ // Send a frame. The timer to request the refresh frame should be cancelled by
+ // the reception, so no refreshes should be requested.
+ EXPECT_CALL(callback, RequestRefreshFrame).Times(0);
+ adapter->OnFrame(CreateFrame());
+ time_controller.AdvanceTime(TimeDelta::Seconds(1));
+ Mock::VerifyAndClearExpectations(&callback);
+}
+
+TEST(FrameCadenceAdapterTest, AcceptsUnconfiguredLayerFeedback) {
+ // This is a regression test for bugs.webrtc.org/14417.
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Zero());
+ auto adapter = CreateAdapter(enabler, time_controller.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{.num_simulcast_layers =
+ 1});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+ time_controller.AdvanceTime(TimeDelta::Zero());
+
+ adapter->UpdateLayerQualityConvergence(2, false);
+ adapter->UpdateLayerStatus(2, false);
+}
+
+class FrameCadenceAdapterSimulcastLayersParamTest
+ : public ::testing::TestWithParam<int> {
+ public:
+ static constexpr int kMaxFpsHz = 8;
+ static constexpr TimeDelta kMinFrameDelay =
+ TimeDelta::Millis(1000 / kMaxFpsHz);
+ static constexpr TimeDelta kIdleFrameDelay =
+ FrameCadenceAdapterInterface::kZeroHertzIdleRepeatRatePeriod;
+
+ FrameCadenceAdapterSimulcastLayersParamTest() {
+ adapter_->Initialize(&callback_);
+ adapter_->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFpsHz});
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+ adapter_->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ const size_t num_spatial_layers = GetParam();
+ adapter_->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{num_spatial_layers});
+ }
+
+ int NumSpatialLayers() const { return GetParam(); }
+
+ protected:
+ ZeroHertzFieldTrialEnabler enabler_;
+ MockCallback callback_;
+ GlobalSimulatedTimeController time_controller_{Timestamp::Zero()};
+ const std::unique_ptr<FrameCadenceAdapterInterface> adapter_{
+ CreateAdapter(enabler_, time_controller_.GetClock())};
+};
+
+TEST_P(FrameCadenceAdapterSimulcastLayersParamTest,
+ LayerReconfigurationResetsConvergenceInfo) {
+ // Assumes layer reconfiguration has just happened.
+ // Verify the state is unconverged.
+ adapter_->OnFrame(CreateFrame());
+ EXPECT_CALL(callback_, OnFrame).Times(kMaxFpsHz);
+ time_controller_.AdvanceTime(kMaxFpsHz * kMinFrameDelay);
+}
+
+TEST_P(FrameCadenceAdapterSimulcastLayersParamTest,
+ IgnoresKeyFrameRequestWhileShortRepeating) {
+ // Plot:
+ // 1. 0 * kMinFrameDelay: Start unconverged. Frame -> adapter.
+ // 2. 1 * kMinFrameDelay: Frame -> callback.
+ // 3. 2 * kMinFrameDelay: 1st short repeat.
+ // Since we're unconverged we assume the process continues.
+ adapter_->OnFrame(CreateFrame());
+ time_controller_.AdvanceTime(2 * kMinFrameDelay);
+ EXPECT_CALL(callback_, RequestRefreshFrame).Times(0);
+ adapter_->ProcessKeyFrameRequest();
+
+ // Expect short repeating as ususal.
+ EXPECT_CALL(callback_, OnFrame).Times(8);
+ time_controller_.AdvanceTime(8 * kMinFrameDelay);
+}
+
+TEST_P(FrameCadenceAdapterSimulcastLayersParamTest,
+ IgnoresKeyFrameRequestJustBeforeIdleRepeating) {
+ // (Only for > 0 spatial layers as we assume not converged with 0 layers)
+ if (NumSpatialLayers() == 0)
+ return;
+
+ // Plot:
+ // 1. 0 * kMinFrameDelay: Start converged. Frame -> adapter.
+ // 2. 1 * kMinFrameDelay: Frame -> callback. New repeat scheduled at
+ // (kMaxFpsHz + 1) * kMinFrameDelay.
+ // 3. kMaxFpsHz * kMinFrameDelay: Process keyframe.
+ // 4. (kMaxFpsHz + N) * kMinFrameDelay (1 <= N <= kMaxFpsHz): Short repeats
+ // due to not converged.
+ for (int i = 0; i != NumSpatialLayers(); i++) {
+ adapter_->UpdateLayerStatus(i, /*enabled=*/true);
+ adapter_->UpdateLayerQualityConvergence(i, /*converged=*/true);
+ }
+ adapter_->OnFrame(CreateFrame());
+ time_controller_.AdvanceTime(kIdleFrameDelay);
+
+ // We process the key frame request kMinFrameDelay before the first idle
+ // repeat should happen. The resulting repeats should happen spaced by
+ // kMinFrameDelay before we get new convergence info.
+ EXPECT_CALL(callback_, RequestRefreshFrame).Times(0);
+ adapter_->ProcessKeyFrameRequest();
+ EXPECT_CALL(callback_, OnFrame).Times(kMaxFpsHz);
+ time_controller_.AdvanceTime(kMaxFpsHz * kMinFrameDelay);
+}
+
+TEST_P(FrameCadenceAdapterSimulcastLayersParamTest,
+ IgnoresKeyFrameRequestShortRepeatsBeforeIdleRepeat) {
+ // (Only for > 0 spatial layers as we assume not converged with 0 layers)
+ if (NumSpatialLayers() == 0)
+ return;
+ // Plot:
+ // 1. 0 * kMinFrameDelay: Start converged. Frame -> adapter.
+ // 2. 1 * kMinFrameDelay: Frame -> callback. New repeat scheduled at
+ // (kMaxFpsHz + 1) * kMinFrameDelay.
+ // 3. 2 * kMinFrameDelay: Process keyframe.
+ // 4. (2 + N) * kMinFrameDelay (1 <= N <= kMaxFpsHz): Short repeats due to not
+ // converged.
+ for (int i = 0; i != NumSpatialLayers(); i++) {
+ adapter_->UpdateLayerStatus(i, /*enabled=*/true);
+ adapter_->UpdateLayerQualityConvergence(i, /*converged=*/true);
+ }
+ adapter_->OnFrame(CreateFrame());
+ time_controller_.AdvanceTime(2 * kMinFrameDelay);
+
+ // We process the key frame request (kMaxFpsHz - 1) * kMinFrameDelay before
+ // the first idle repeat should happen. The resulting repeats should happen
+ // spaced kMinFrameDelay before we get new convergence info.
+ EXPECT_CALL(callback_, RequestRefreshFrame).Times(0);
+ adapter_->ProcessKeyFrameRequest();
+ EXPECT_CALL(callback_, OnFrame).Times(kMaxFpsHz);
+ time_controller_.AdvanceTime(kMaxFpsHz * kMinFrameDelay);
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+ FrameCadenceAdapterSimulcastLayersParamTest,
+ Values(0, 1, 2));
+
+class ZeroHertzLayerQualityConvergenceTest : public ::testing::Test {
+ public:
+ static constexpr TimeDelta kMinFrameDelay = TimeDelta::Millis(100);
+ static constexpr TimeDelta kIdleFrameDelay =
+ FrameCadenceAdapterInterface::kZeroHertzIdleRepeatRatePeriod;
+
+ ZeroHertzLayerQualityConvergenceTest() {
+ adapter_->Initialize(&callback_);
+ adapter_->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{
+ /*num_simulcast_layers=*/2});
+ adapter_->OnConstraintsChanged(VideoTrackSourceConstraints{
+ /*min_fps=*/0, /*max_fps=*/TimeDelta::Seconds(1) / kMinFrameDelay});
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+ }
+
+ void PassFrame() { adapter_->OnFrame(CreateFrame()); }
+
+ void ExpectFrameEntriesAtDelaysFromNow(
+ std::initializer_list<TimeDelta> list) {
+ Timestamp origin = time_controller_.GetClock()->CurrentTime();
+ for (auto delay : list) {
+ EXPECT_CALL(callback_, OnFrame(origin + delay, _, _));
+ time_controller_.AdvanceTime(origin + delay -
+ time_controller_.GetClock()->CurrentTime());
+ }
+ }
+
+ void ScheduleDelayed(TimeDelta delay, absl::AnyInvocable<void() &&> task) {
+ TaskQueueBase::Current()->PostDelayedTask(std::move(task), delay);
+ }
+
+ protected:
+ ZeroHertzFieldTrialEnabler field_trial_enabler_;
+ MockCallback callback_;
+ GlobalSimulatedTimeController time_controller_{Timestamp::Zero()};
+ std::unique_ptr<FrameCadenceAdapterInterface> adapter_{
+ CreateAdapter(field_trial_enabler_, time_controller_.GetClock())};
+};
+
+TEST_F(ZeroHertzLayerQualityConvergenceTest, InitialStateUnconverged) {
+ // As the layer count is just configured, assume we start out as unconverged.
+ PassFrame();
+ ExpectFrameEntriesAtDelaysFromNow({
+ 1 * kMinFrameDelay, // Original frame emitted
+ 2 * kMinFrameDelay, // Short repeats.
+ 3 * kMinFrameDelay, // ...
+ });
+}
+
+TEST_F(ZeroHertzLayerQualityConvergenceTest, UnconvergedAfterLayersEnabled) {
+ // With newly enabled layers we assume quality is unconverged.
+ adapter_->UpdateLayerStatus(0, /*enabled=*/true);
+ adapter_->UpdateLayerStatus(1, /*enabled=*/true);
+ PassFrame();
+ ExpectFrameEntriesAtDelaysFromNow({
+ kMinFrameDelay, // Original frame emitted
+ 2 * kMinFrameDelay, // Unconverged repeats.
+ 3 * kMinFrameDelay, // ...
+ });
+}
+
+TEST_F(ZeroHertzLayerQualityConvergenceTest,
+ RepeatsPassedFramesUntilConvergence) {
+ ScheduleDelayed(TimeDelta::Zero(), [&] {
+ adapter_->UpdateLayerStatus(0, /*enabled=*/true);
+ adapter_->UpdateLayerStatus(1, /*enabled=*/true);
+ PassFrame();
+ });
+ ScheduleDelayed(2.5 * kMinFrameDelay, [&] {
+ adapter_->UpdateLayerQualityConvergence(/*spatial_index=*/1, true);
+ });
+ ScheduleDelayed(3.5 * kMinFrameDelay, [&] {
+ adapter_->UpdateLayerQualityConvergence(/*spatial_index=*/0, true);
+ });
+ ScheduleDelayed(8 * kMinFrameDelay, [&] { PassFrame(); });
+ ScheduleDelayed(9.5 * kMinFrameDelay, [&] {
+ adapter_->UpdateLayerQualityConvergence(/*spatial_index=*/0, true);
+ });
+ ScheduleDelayed(10.5 * kMinFrameDelay, [&] {
+ adapter_->UpdateLayerQualityConvergence(/*spatial_index=*/1, true);
+ });
+ ExpectFrameEntriesAtDelaysFromNow({
+ kMinFrameDelay, // Original frame emitted
+ 2 * kMinFrameDelay, // Repeat from kMinFrameDelay.
+
+ // 2.5 * kMinFrameDelay: Converged in layer 1, layer 0 still unconverged.
+ 3 * kMinFrameDelay, // Repeat from 2 * kMinFrameDelay.
+
+ // 3.5 * kMinFrameDelay: Converged in layer 0 as well.
+ 4 * kMinFrameDelay, // Repeat from 3 * kMinFrameDelay. An idle repeat is
+ // scheduled for kIdleFrameDelay + 3 *
+ // kMinFrameDelay.
+
+ // A new frame is passed at 8 * kMinFrameDelay.
+ 9 * kMinFrameDelay, // Original frame emitted
+
+ // 9.5 * kMinFrameDelay: Converged in layer 0, layer 1 still unconverged.
+ 10 * kMinFrameDelay, // Repeat from 9 * kMinFrameDelay.
+ // 10.5 * kMinFrameDelay: Converged in layer 0 as well.
+ 11 * kMinFrameDelay, // Idle repeats from 1000.
+ 11 * kMinFrameDelay + kIdleFrameDelay, // ...
+ 11 * kMinFrameDelay + 2 * kIdleFrameDelay, // ...
+ // ...
+ });
+}
+
+class FrameCadenceAdapterMetricsTest : public ::testing::Test {
+ public:
+ FrameCadenceAdapterMetricsTest() : time_controller_(Timestamp::Millis(1)) {
+ metrics::Reset();
+ }
+ void DepleteTaskQueues() { time_controller_.AdvanceTime(TimeDelta::Zero()); }
+
+ protected:
+ GlobalSimulatedTimeController time_controller_;
+};
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsNoUmasWithNoFrameTransfer) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, nullptr);
+ adapter->Initialize(&callback);
+ adapter->OnConstraintsChanged(
+ VideoTrackSourceConstraints{absl::nullopt, absl::nullopt});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{absl::nullopt, 1});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{2, 3});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{4, 4});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{5, absl::nullopt});
+ DepleteTaskQueues();
+ EXPECT_TRUE(metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne")
+ .empty());
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsNoUmasWithoutEnabledContentType) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->OnFrame(CreateFrame());
+ adapter->OnConstraintsChanged(
+ VideoTrackSourceConstraints{absl::nullopt, absl::nullopt});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{absl::nullopt, 1});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{2, 3});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{4, 4});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{5, absl::nullopt});
+ DepleteTaskQueues();
+ EXPECT_TRUE(metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne")
+ .empty());
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsNoConstraintsIfUnsetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Exists"),
+ ElementsAre(Pair(false, 1)));
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsEmptyConstraintsIfSetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(
+ VideoTrackSourceConstraints{absl::nullopt, absl::nullopt});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Exists"),
+ ElementsAre(Pair(true, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists"),
+ ElementsAre(Pair(false, 1)));
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value")
+ .empty());
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists"),
+ ElementsAre(Pair(false, 1)));
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne")
+ .empty());
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsMaxConstraintIfSetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(
+ VideoTrackSourceConstraints{absl::nullopt, 2.0});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists"),
+ ElementsAre(Pair(false, 1)));
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value")
+ .empty());
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists"),
+ ElementsAre(Pair(true, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value"),
+ ElementsAre(Pair(2.0, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max"),
+ ElementsAre(Pair(2.0, 1)));
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne")
+ .empty());
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsMinConstraintIfSetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(
+ VideoTrackSourceConstraints{3.0, absl::nullopt});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists"),
+ ElementsAre(Pair(true, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value"),
+ ElementsAre(Pair(3.0, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists"),
+ ElementsAre(Pair(false, 1)));
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_TRUE(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne")
+ .empty());
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsMinGtMaxConstraintIfSetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{5.0, 4.0});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Exists"),
+ ElementsAre(Pair(true, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Min.Value"),
+ ElementsAre(Pair(5.0, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Exists"),
+ ElementsAre(Pair(true, 1)));
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.Max.Value"),
+ ElementsAre(Pair(4.0, 1)));
+ EXPECT_TRUE(
+ metrics::Samples("WebRTC.Screenshare.FrameRateConstraints.MinUnset.Max")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min")
+ .empty());
+ EXPECT_TRUE(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max")
+ .empty());
+ EXPECT_THAT(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne"),
+ ElementsAre(Pair(60 * 5.0 + 4.0 - 1, 1)));
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsMinLtMaxConstraintIfSetOnFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{4.0, 5.0});
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Min"),
+ ElementsAre(Pair(4.0, 1)));
+ EXPECT_THAT(metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.MinLessThanMax.Max"),
+ ElementsAre(Pair(5.0, 1)));
+ EXPECT_THAT(
+ metrics::Samples(
+ "WebRTC.Screenshare.FrameRateConstraints.60MinPlusMaxMinusOne"),
+ ElementsAre(Pair(60 * 4.0 + 5.0 - 1, 1)));
+}
+
+TEST_F(FrameCadenceAdapterMetricsTest, RecordsTimeUntilFirstFrame) {
+ MockCallback callback;
+ test::ScopedKeyValueConfig no_field_trials;
+ auto adapter = CreateAdapter(no_field_trials, time_controller_.GetClock());
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 5.0});
+ time_controller_.AdvanceTime(TimeDelta::Millis(666));
+ adapter->OnFrame(CreateFrame());
+ DepleteTaskQueues();
+ EXPECT_THAT(
+ metrics::Samples("WebRTC.Screenshare.ZeroHz.TimeUntilFirstFrameMs"),
+ ElementsAre(Pair(666, 1)));
+}
+
+TEST(FrameCadenceAdapterRealTimeTest, TimestampsDoNotDrift) {
+ // This regression test must be performed in realtime because of limitations
+ // in GlobalSimulatedTimeController.
+ //
+ // We sleep for a long while in OnFrame when a repeat was scheduled which
+ // should reflect in accordingly increased ntp_time_ms() and timestamp_us() in
+ // the repeated frames.
+ auto factory = CreateDefaultTaskQueueFactory();
+ auto queue =
+ factory->CreateTaskQueue("test", TaskQueueFactory::Priority::NORMAL);
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ Clock* clock = Clock::GetRealTimeClock();
+ std::unique_ptr<FrameCadenceAdapterInterface> adapter;
+ int frame_counter = 0;
+ int64_t original_ntp_time_ms;
+ int64_t original_timestamp_us;
+ rtc::Event event;
+ queue->PostTask([&] {
+ adapter = CreateAdapter(enabler, clock);
+ adapter->Initialize(&callback);
+ adapter->SetZeroHertzModeEnabled(
+ FrameCadenceAdapterInterface::ZeroHertzModeParams{});
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 30});
+ auto frame = CreateFrame();
+ original_ntp_time_ms = clock->CurrentNtpInMilliseconds();
+ frame.set_ntp_time_ms(original_ntp_time_ms);
+ original_timestamp_us = clock->CurrentTime().us();
+ frame.set_timestamp_us(original_timestamp_us);
+ constexpr int kSleepMs = rtc::kNumMillisecsPerSec / 2;
+ EXPECT_CALL(callback, OnFrame)
+ .WillRepeatedly(
+ Invoke([&](Timestamp, int, const VideoFrame& incoming_frame) {
+ ++frame_counter;
+ // Avoid the first OnFrame and sleep on the second.
+ if (frame_counter == 2) {
+ SleepMs(kSleepMs);
+ } else if (frame_counter == 3) {
+ EXPECT_GE(incoming_frame.ntp_time_ms(),
+ original_ntp_time_ms + kSleepMs);
+ EXPECT_GE(incoming_frame.timestamp_us(),
+ original_timestamp_us + kSleepMs);
+ event.Set();
+ }
+ }));
+ adapter->OnFrame(frame);
+ });
+ event.Wait(rtc::Event::kForever);
+ rtc::Event finalized;
+ queue->PostTask([&] {
+ adapter = nullptr;
+ finalized.Set();
+ });
+ finalized.Wait(rtc::Event::kForever);
+}
+
+} // namespace
+} // namespace webrtc