FrameCadenceAdapter: ensure frame arrival after drop.
This CL accomplishes three things:
1) It enables feeding frame drop indications into the
AdaptedVideoTrackSource for the benefit of downstream projects.
2) Under zero hertz source delivery, a discarded frame ending a
sequence of frames which happened to contain important information
can be seen as a capture freeze. Avoid this by starting requesting
refresh frames after a grace period.
3) It changes the duration until first refresh frame requests on new
streams to three frame periods.
Bug: chromium:1324120, chromium:1336952
Change-Id: I0214852f1a26540588f6c193dd88a65c34ec0d99
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/265871
Reviewed-by: Erik Språng <sprang@webrtc.org>
Commit-Queue: Markus Handell <handellm@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#37238}
diff --git a/media/base/adapted_video_track_source.cc b/media/base/adapted_video_track_source.cc
index f8f8f2d..816ada5 100644
--- a/media/base/adapted_video_track_source.cc
+++ b/media/base/adapted_video_track_source.cc
@@ -61,6 +61,10 @@
}
}
+void AdaptedVideoTrackSource::OnFrameDropped() {
+ broadcaster_.OnDiscardedFrame();
+}
+
void AdaptedVideoTrackSource::AddOrUpdateSink(
rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
const rtc::VideoSinkWants& wants) {
diff --git a/media/base/adapted_video_track_source.h b/media/base/adapted_video_track_source.h
index 1386fbd..1c3e0b6 100644
--- a/media/base/adapted_video_track_source.h
+++ b/media/base/adapted_video_track_source.h
@@ -45,6 +45,8 @@
// plain memory frame, it is rotated. Subclasses producing native frames must
// handle apply_rotation() themselves.
void OnFrame(const webrtc::VideoFrame& frame);
+ // Indication from source that a frame was dropped.
+ void OnFrameDropped();
// Reports the appropriate frame size after adaptation. Returns true
// if a frame is wanted. Returns false if there are no interested
diff --git a/video/frame_cadence_adapter.cc b/video/frame_cadence_adapter.cc
index b6e21d6..94fd318 100644
--- a/video/frame_cadence_adapter.cc
+++ b/video/frame_cadence_adapter.cc
@@ -120,6 +120,9 @@
absl::optional<uint32_t> GetInputFrameRateFps() override;
void UpdateFrameRate() override {}
+ // Notified on dropped frames.
+ void OnDiscardedFrame();
+
// Conditionally requests a refresh frame via
// Callback::RequestRefreshFrame.
void ProcessKeyFrameRequest();
@@ -182,6 +185,10 @@
// Returns the repeat duration depending on if it's an idle repeat or not.
TimeDelta RepeatDuration(bool idle_repeat) const
RTC_RUN_ON(sequence_checker_);
+ // Unless timer already running, starts repeatedly requesting refresh frames
+ // after a grace_period. If a frame appears before the grace_period has
+ // passed, the request is cancelled.
+ void MaybeStartRefreshFrameRequester() RTC_RUN_ON(sequence_checker_);
TaskQueueBase* const queue_;
Clock* const clock_;
@@ -233,7 +240,7 @@
// VideoFrameSink overrides.
void OnFrame(const VideoFrame& frame) override;
- void OnDiscardedFrame() override { callback_->OnDiscardedFrame(); }
+ void OnDiscardedFrame() override;
void OnConstraintsChanged(
const VideoTrackSourceConstraints& constraints) override;
@@ -301,12 +308,7 @@
double max_fps)
: queue_(queue), clock_(clock), callback_(callback), max_fps_(max_fps) {
sequence_checker_.Detach();
- refresh_frame_requester_ = RepeatingTaskHandle::Start(queue_, [this] {
- RTC_DLOG(LS_VERBOSE) << __func__ << " RequestRefreshFrame";
- if (callback_)
- callback_->RequestRefreshFrame();
- return frame_delay_;
- });
+ MaybeStartRefreshFrameRequester();
}
void ZeroHertzAdapterMode::ReconfigureParameters(
@@ -384,6 +386,17 @@
frame_delay_.ms());
}
+void ZeroHertzAdapterMode::OnDiscardedFrame() {
+ RTC_DCHECK_RUN_ON(&sequence_checker_);
+ RTC_DLOG(LS_VERBOSE) << "ZeroHertzAdapterMode::" << __func__;
+
+ // Under zero hertz source delivery, a discarded frame ending a sequence of
+ // frames which happened to contain important information can be seen as a
+ // capture freeze. Avoid this by starting requesting refresh frames after a
+ // grace period.
+ MaybeStartRefreshFrameRequester();
+}
+
absl::optional<uint32_t> ZeroHertzAdapterMode::GetInputFrameRateFps() {
RTC_DCHECK_RUN_ON(&sequence_checker_);
return max_fps_;
@@ -552,6 +565,23 @@
: frame_delay_;
}
+// RTC_RUN_ON(&sequence_checker_)
+void ZeroHertzAdapterMode::MaybeStartRefreshFrameRequester() {
+ RTC_DLOG(LS_VERBOSE) << __func__;
+ if (!refresh_frame_requester_.Running()) {
+ refresh_frame_requester_ = RepeatingTaskHandle::DelayedStart(
+ queue_,
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod *
+ frame_delay_,
+ [this] {
+ RTC_DLOG(LS_VERBOSE) << __func__ << " RequestRefreshFrame";
+ if (callback_)
+ callback_->RequestRefreshFrame();
+ return frame_delay_;
+ });
+ }
+}
+
FrameCadenceAdapterImpl::FrameCadenceAdapterImpl(
Clock* clock,
TaskQueueBase* queue,
@@ -644,6 +674,16 @@
}));
}
+void FrameCadenceAdapterImpl::OnDiscardedFrame() {
+ callback_->OnDiscardedFrame();
+ queue_->PostTask(ToQueuedTask(safety_.flag(), [this] {
+ RTC_DCHECK_RUN_ON(queue_);
+ if (zero_hertz_adapter_) {
+ zero_hertz_adapter_->OnDiscardedFrame();
+ }
+ }));
+}
+
void FrameCadenceAdapterImpl::OnConstraintsChanged(
const VideoTrackSourceConstraints& constraints) {
RTC_LOG(LS_INFO) << __func__ << " this " << this << " min_fps "
diff --git a/video/frame_cadence_adapter.h b/video/frame_cadence_adapter.h
index 40e9805..149a915 100644
--- a/video/frame_cadence_adapter.h
+++ b/video/frame_cadence_adapter.h
@@ -41,6 +41,9 @@
// and some worst case RTT.
static constexpr TimeDelta kZeroHertzIdleRepeatRatePeriod =
TimeDelta::Millis(1000);
+ // The number of frame periods to wait for new frames until starting to
+ // request refresh frames.
+ static constexpr int kOnDiscardedFrameRefreshFramePeriod = 3;
struct ZeroHertzModeParams {
// The number of simulcast layers used in this configuration.
diff --git a/video/frame_cadence_adapter_unittest.cc b/video/frame_cadence_adapter_unittest.cc
index 9bd312a..3a2767a 100644
--- a/video/frame_cadence_adapter_unittest.cc
+++ b/video/frame_cadence_adapter_unittest.cc
@@ -368,9 +368,13 @@
adapter->Initialize(&callback);
adapter->SetZeroHertzModeEnabled(
FrameCadenceAdapterInterface::ZeroHertzModeParams{});
- adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 10});
+ constexpr int kMaxFps = 10;
+ adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
EXPECT_CALL(callback, RequestRefreshFrame);
- time_controller.AdvanceTime(TimeDelta::Zero());
+ time_controller.AdvanceTime(
+ TimeDelta::Seconds(1) *
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod /
+ kMaxFps);
adapter->ProcessKeyFrameRequest();
}
@@ -400,10 +404,14 @@
constexpr int kMaxFps = 10;
adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
- // We should see max_fps + 1 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);
+ // 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());
@@ -411,6 +419,85 @@
time_controller.AdvanceTime(TimeDelta::Seconds(1));
}
+TEST(FrameCadenceAdapterTest, RequestsRefreshAfterFrameDrop) {
+ ZeroHertzFieldTrialEnabler enabler;
+ MockCallback callback;
+ GlobalSimulatedTimeController time_controller(Timestamp::Millis(0));
+ 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::Millis(0));
+ 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);
+}
+
class FrameCadenceAdapterSimulcastLayersParamTest
: public ::testing::TestWithParam<int> {
public:
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index 5436df8..575a8e2 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -26,6 +26,7 @@
#include "api/test/mock_video_encoder.h"
#include "api/test/mock_video_encoder_factory.h"
#include "api/units/data_rate.h"
+#include "api/units/time_delta.h"
#include "api/video/builtin_video_bitrate_allocator_factory.h"
#include "api/video/i420_buffer.h"
#include "api/video/nv12_buffer.h"
@@ -9410,8 +9411,12 @@
// synchronized.
EXPECT_CALL(mock_source, RequestRefreshFrame);
video_stream_encoder->SendKeyFrame();
- adapter_ptr->OnConstraintsChanged(VideoTrackSourceConstraints{0, 30});
- factory.DepleteTaskQueues();
+ constexpr int kMaxFps = 30;
+ adapter_ptr->OnConstraintsChanged(VideoTrackSourceConstraints{0, kMaxFps});
+ factory.GetTimeController()->AdvanceTime(
+ TimeDelta::Seconds(1) *
+ FrameCadenceAdapterInterface::kOnDiscardedFrameRefreshFramePeriod /
+ kMaxFps);
}
} // namespace webrtc