Add FrameBufferProxy in prep for FrameBuffer3
This is a delegate that is used by video_receive_stream2 to handle frame
buffer tasks like threading, and stats. This will be used in a follow up
to use FrameBuffer3 as a strategy selected by field trial.
Unit-tests will be used in follow-up CLs containing Frame Buffer 3, and
are expected to work with both Frame buffer proxy versions.
Change-Id: I524279343d60a348d044d9085d618f12d7bf3a23
Bug: webrtc:13343
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/241605
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Commit-Queue: Evan Shrubsole <eshr@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35803}
diff --git a/video/frame_buffer_proxy_unittest.cc b/video/frame_buffer_proxy_unittest.cc
new file mode 100644
index 0000000..c8aa114
--- /dev/null
+++ b/video/frame_buffer_proxy_unittest.cc
@@ -0,0 +1,638 @@
+/*
+ * Copyright (c) 2022 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_buffer_proxy.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "api/video/video_content_type.h"
+#include "rtc_base/event.h"
+#include "test/field_trial.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "test/run_loop.h"
+#include "test/time_controller/simulated_time_controller.h"
+
+using ::testing::AllOf;
+using ::testing::Contains;
+using ::testing::Each;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::Matches;
+using ::testing::Ne;
+using ::testing::Not;
+using ::testing::Optional;
+using ::testing::Pointee;
+using ::testing::SizeIs;
+
+namespace webrtc {
+
+// For test printing.
+void PrintTo(const EncodedFrame& frame, std::ostream* os) {
+ *os << "EncodedFrame with id=" << frame.Id() << " rtp=" << frame.Timestamp()
+ << " size=" << frame.size() << " refs=[";
+ for (size_t ref = 0; ref < frame.num_references; ++ref) {
+ *os << frame.references[ref] << ",";
+ }
+ *os << "]";
+}
+
+namespace {
+
+constexpr size_t kFrameSize = 10;
+constexpr uint32_t kFps30Rtp = 90000 / 30;
+constexpr TimeDelta kFps30Delay = 1 / Frequency::Hertz(30);
+const VideoPlayoutDelay kZeroPlayoutDelay = {0, 0};
+constexpr Timestamp kClockStart = Timestamp::Millis(1000);
+
+class FakeEncodedFrame : public EncodedFrame {
+ public:
+ // Always 10ms delay and on time.
+ int64_t ReceivedTime() const override {
+ if (Timestamp() == 0)
+ return kClockStart.ms();
+ return TimeDelta::Seconds(Timestamp() / 90000.0).ms() + kClockStart.ms();
+ }
+ int64_t RenderTime() const override { return _renderTimeMs; }
+};
+
+MATCHER_P(FrameWithId, id, "") {
+ return Matches(Eq(id))(arg.Id());
+}
+
+MATCHER_P(FrameWithSize, id, "") {
+ return Matches(Eq(id))(arg.size());
+}
+
+class Builder {
+ public:
+ Builder& Time(uint32_t rtp_timestamp) {
+ rtp_timestamp_ = rtp_timestamp;
+ return *this;
+ }
+ Builder& Id(int64_t frame_id) {
+ frame_id_ = frame_id;
+ return *this;
+ }
+ Builder& AsLast() {
+ last_spatial_layer_ = true;
+ return *this;
+ }
+ Builder& Refs(const std::vector<int64_t>& references) {
+ references_ = references;
+ return *this;
+ }
+ Builder& PlayoutDelay(VideoPlayoutDelay playout_delay) {
+ playout_delay_ = playout_delay;
+ return *this;
+ }
+ Builder& SpatialLayer(int spatial_layer) {
+ spatial_layer_ = spatial_layer;
+ return *this;
+ }
+
+ std::unique_ptr<FakeEncodedFrame> Build() {
+ RTC_CHECK_LE(references_.size(), EncodedFrame::kMaxFrameReferences);
+ RTC_CHECK(rtp_timestamp_);
+ RTC_CHECK(frame_id_);
+
+ auto frame = std::make_unique<FakeEncodedFrame>();
+ frame->SetTimestamp(*rtp_timestamp_);
+ frame->SetId(*frame_id_);
+ frame->is_last_spatial_layer = last_spatial_layer_;
+ frame->SetEncodedData(EncodedImageBuffer::Create(kFrameSize));
+
+ if (playout_delay_)
+ frame->SetPlayoutDelay(*playout_delay_);
+
+ for (int64_t ref : references_) {
+ frame->references[frame->num_references] = ref;
+ frame->num_references++;
+ }
+ if (spatial_layer_) {
+ frame->SetSpatialIndex(spatial_layer_);
+ }
+
+ return frame;
+ }
+
+ private:
+ absl::optional<uint32_t> rtp_timestamp_;
+ absl::optional<int64_t> frame_id_;
+ absl::optional<VideoPlayoutDelay> playout_delay_;
+ absl::optional<int> spatial_layer_;
+ bool last_spatial_layer_ = false;
+ std::vector<int64_t> references_;
+};
+
+class VCMReceiveStatisticsCallbackMock : public VCMReceiveStatisticsCallback {
+ public:
+ MOCK_METHOD(void,
+ OnCompleteFrame,
+ (bool is_keyframe,
+ size_t size_bytes,
+ VideoContentType content_type),
+ (override));
+ MOCK_METHOD(void, OnDroppedFrames, (uint32_t num_dropped), (override));
+ MOCK_METHOD(void,
+ OnFrameBufferTimingsUpdated,
+ (int max_decode_ms,
+ int current_delay_ms,
+ int target_delay_ms,
+ int jitter_buffer_ms,
+ int min_playout_delay_ms,
+ int render_delay_ms),
+ (override));
+ MOCK_METHOD(void,
+ OnTimingFrameInfoUpdated,
+ (const TimingFrameInfo& info),
+ (override));
+};
+} // namespace
+
+constexpr auto kMaxWaitForKeyframe = TimeDelta::Millis(500);
+constexpr auto kMaxWaitForFrame = TimeDelta::Millis(1500);
+class FrameBufferProxyTest : public ::testing::TestWithParam<std::string>,
+ public FrameSchedulingReceiver {
+ public:
+ FrameBufferProxyTest()
+ : field_trials_(GetParam()),
+ time_controller_(kClockStart),
+ clock_(time_controller_.GetClock()),
+ decode_queue_(time_controller_.GetTaskQueueFactory()->CreateTaskQueue(
+ "decode_queue",
+ TaskQueueFactory::Priority::NORMAL)),
+ timing_(clock_),
+ proxy_(FrameBufferProxy::CreateFromFieldTrial(clock_,
+ run_loop_.task_queue(),
+ &timing_,
+ &stats_callback_,
+ &decode_queue_,
+ this,
+ kMaxWaitForKeyframe,
+ kMaxWaitForFrame)) {
+ // Avoid starting with negative render times.
+ timing_.set_min_playout_delay(10);
+
+ ON_CALL(stats_callback_, OnDroppedFrames)
+ .WillByDefault(
+ [this](auto num_dropped) { dropped_frames_ += num_dropped; });
+ }
+
+ ~FrameBufferProxyTest() override {
+ if (proxy_) {
+ proxy_->StopOnWorker();
+ }
+ }
+
+ void OnEncodedFrame(std::unique_ptr<EncodedFrame> frame) override {
+ last_frame_ = std::move(frame);
+ run_loop_.Quit();
+ }
+
+ void OnDecodableFrameTimeout(TimeDelta wait_time) override {
+ timeouts_++;
+ run_loop_.Quit();
+ }
+
+ bool WaitForFrameOrTimeout(TimeDelta wait) {
+ if (NewFrameOrTimeout()) {
+ return true;
+ }
+ run_loop_.PostTask([&] { time_controller_.AdvanceTime(wait); });
+ run_loop_.PostTask([&] {
+ // If run loop posted to a task queue, flush that.
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+
+ run_loop_.PostTask([&] {
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+ run_loop_.Quit();
+ });
+ });
+ run_loop_.Run();
+ return NewFrameOrTimeout();
+ }
+
+ void StartNextDecode() {
+ ResetLastResult();
+ proxy_->StartNextDecode(false);
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+ }
+
+ void StartNextDecodeForceKeyframe() {
+ ResetLastResult();
+ proxy_->StartNextDecode(true);
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+ }
+
+ void ResetLastResult() {
+ last_frame_.reset();
+ last_timeouts_ = timeouts_;
+ }
+
+ int timeouts() const { return timeouts_; }
+ EncodedFrame* last_frame() const { return last_frame_.get(); }
+ int dropped_frames() const { return dropped_frames_; }
+
+ protected:
+ test::ScopedFieldTrials field_trials_;
+ GlobalSimulatedTimeController time_controller_;
+ Clock* const clock_;
+ test::RunLoop run_loop_;
+ rtc::TaskQueue decode_queue_;
+ VCMTiming timing_;
+ ::testing::NiceMock<VCMReceiveStatisticsCallbackMock> stats_callback_;
+ std::unique_ptr<FrameBufferProxy> proxy_;
+
+ private:
+ bool NewFrameOrTimeout() const {
+ return last_frame_ || timeouts_ != last_timeouts_;
+ }
+
+ int timeouts_ = 0;
+ int last_timeouts_ = 0;
+ std::unique_ptr<EncodedFrame> last_frame_;
+ uint32_t dropped_frames_ = 0;
+};
+
+TEST_P(FrameBufferProxyTest, InitialTimeoutAfterKeyframeTimeoutPeriod) {
+ StartNextDecodeForceKeyframe();
+ // No frame insterted. Timeout expected.
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForKeyframe));
+ EXPECT_EQ(timeouts(), 1);
+
+ // No new timeout set since receiver has not started new decode.
+ ResetLastResult();
+ EXPECT_FALSE(WaitForFrameOrTimeout(kMaxWaitForKeyframe));
+ EXPECT_EQ(timeouts(), 1);
+
+ // Now that receiver has asked for new frame, a new timeout can occur.
+ StartNextDecodeForceKeyframe();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForKeyframe));
+ EXPECT_EQ(timeouts(), 2);
+}
+
+TEST_P(FrameBufferProxyTest, KeyFramesAreScheduled) {
+ StartNextDecodeForceKeyframe();
+ time_controller_.AdvanceTime(TimeDelta::Millis(50));
+
+ auto frame = Builder().Id(0).Time(0).AsLast().Build();
+ proxy_->InsertFrame(std::move(frame));
+
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+
+ ASSERT_THAT(last_frame(), Pointee(FrameWithId(0)));
+ EXPECT_EQ(timeouts(), 0);
+}
+
+TEST_P(FrameBufferProxyTest, DeltaFrameTimeoutAfterKeyframeExtracted) {
+ StartNextDecodeForceKeyframe();
+
+ time_controller_.AdvanceTime(TimeDelta::Millis(50));
+ auto frame = Builder().Id(0).Time(0).AsLast().Build();
+ proxy_->InsertFrame(std::move(frame));
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForKeyframe));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecode();
+ time_controller_.AdvanceTime(TimeDelta::Millis(50));
+
+ // Timeouts should now happen at the normal frequency.
+ const int expected_timeouts = 5;
+ for (int i = 0; i < expected_timeouts; ++i) {
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForFrame));
+ StartNextDecode();
+ }
+
+ EXPECT_EQ(timeouts(), expected_timeouts);
+}
+
+TEST_P(FrameBufferProxyTest, DependantFramesAreScheduled) {
+ StartNextDecodeForceKeyframe();
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecode();
+
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).AsLast().Refs({0}).Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(1)));
+ EXPECT_EQ(timeouts(), 0);
+}
+
+TEST_P(FrameBufferProxyTest, SpatialLayersAreScheduled) {
+ StartNextDecodeForceKeyframe();
+ proxy_->InsertFrame(Builder().Id(0).SpatialLayer(0).Time(0).Build());
+ proxy_->InsertFrame(Builder().Id(1).SpatialLayer(1).Time(0).Build());
+ proxy_->InsertFrame(Builder().Id(2).SpatialLayer(2).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(),
+ Pointee(AllOf(FrameWithId(0), FrameWithSize(3 * kFrameSize))));
+
+ proxy_->InsertFrame(Builder().Id(3).Time(kFps30Rtp).SpatialLayer(0).Build());
+ proxy_->InsertFrame(Builder().Id(4).Time(kFps30Rtp).SpatialLayer(1).Build());
+ proxy_->InsertFrame(
+ Builder().Id(5).Time(kFps30Rtp).SpatialLayer(2).AsLast().Build());
+
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay * 10));
+ EXPECT_THAT(last_frame(),
+ Pointee(AllOf(FrameWithId(3), FrameWithSize(3 * kFrameSize))));
+ EXPECT_EQ(timeouts(), 0);
+}
+
+TEST_P(FrameBufferProxyTest, OutstandingFrameTasksAreCancelledAfterDeletion) {
+ StartNextDecodeForceKeyframe();
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ // Get keyframe. Delta frame should now be scheduled.
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecode();
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).AsLast().Refs({0}).Build());
+ proxy_->StopOnWorker();
+ // Wait for 2x max wait time. Since we stopped, this should cause no timeouts
+ // or frame-ready callbacks.
+ EXPECT_FALSE(WaitForFrameOrTimeout(kMaxWaitForFrame * 2));
+ EXPECT_EQ(timeouts(), 0);
+}
+
+TEST_P(FrameBufferProxyTest, FramesWaitForDecoderToComplete) {
+ StartNextDecodeForceKeyframe();
+
+ // Start with a keyframe.
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ ResetLastResult();
+ // Insert a delta frame.
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).AsLast().Refs({0}).Build());
+
+ // Advancing time should not result in a frame since the scheduler has not
+ // been signalled that we are ready.
+ EXPECT_FALSE(WaitForFrameOrTimeout(kFps30Delay));
+ // Signal ready.
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(1)));
+}
+
+TEST_P(FrameBufferProxyTest, LateFrameDropped) {
+ StartNextDecodeForceKeyframe();
+ // F1
+ // /
+ // F0 --> F2
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ // Start with a keyframe.
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecode();
+
+ // Simulate late F1 which arrives after F2.
+ time_controller_.AdvanceTime(kFps30Delay * 2);
+ proxy_->InsertFrame(
+ Builder().Id(2).Time(2 * kFps30Rtp).AsLast().Refs({0}).Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(2)));
+
+ StartNextDecode();
+
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(1 * kFps30Rtp).AsLast().Refs({0}).Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForFrame));
+ // Confirm frame 1 is never scheduled by timing out.
+ EXPECT_EQ(timeouts(), 1);
+}
+
+TEST_P(FrameBufferProxyTest, FramesFastForwardOnSystemHalt) {
+ StartNextDecodeForceKeyframe();
+ // F1
+ // /
+ // F0 --> F2
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+
+ // Start with a keyframe.
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).AsLast().Refs({0}).Build());
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(2).Time(2 * kFps30Rtp).AsLast().Refs({0}).Build());
+
+ // Halting time should result in F1 being skipped.
+ time_controller_.AdvanceTime(kFps30Delay * 2);
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(2)));
+ EXPECT_EQ(dropped_frames(), 1);
+}
+
+TEST_P(FrameBufferProxyTest, ForceKeyFrame) {
+ StartNextDecodeForceKeyframe();
+ // Initial keyframe.
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecodeForceKeyframe();
+
+ // F2 is the next keyframe, and should be extracted since a keyframe was
+ // forced.
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).AsLast().Refs({0}).Build());
+ proxy_->InsertFrame(Builder().Id(2).Time(kFps30Rtp * 2).AsLast().Build());
+
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForFrame));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(2)));
+}
+
+TEST_P(FrameBufferProxyTest, SlowDecoderDropsTemporalLayers) {
+ StartNextDecodeForceKeyframe();
+ // 2 temporal layers, at 15fps per layer to make 30fps total.
+ // Decoder is slower than 30fps, so last_frame() will be skipped.
+ // F1 --> F3 --> F5
+ // / / /
+ // F0 --> F2 --> F4
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ // Keyframe received.
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+ // Don't start next decode until slow delay.
+
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(1 * kFps30Rtp).Refs({0}).AsLast().Build());
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(2).Time(2 * kFps30Rtp).Refs({0}).AsLast().Build());
+
+ // Simulate decode taking 3x FPS rate.
+ time_controller_.AdvanceTime(kFps30Delay * 1.5);
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay * 2));
+ // F2 is the best frame since decoding was so slow that F1 is too old.
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(2)));
+ EXPECT_EQ(dropped_frames(), 1);
+ time_controller_.AdvanceTime(kFps30Delay / 2);
+
+ proxy_->InsertFrame(
+ Builder().Id(3).Time(3 * kFps30Rtp).Refs({1, 2}).AsLast().Build());
+ time_controller_.AdvanceTime(kFps30Delay / 2);
+ proxy_->InsertFrame(
+ Builder().Id(4).Time(4 * kFps30Rtp).Refs({2}).AsLast().Build());
+ time_controller_.AdvanceTime(kFps30Delay / 2);
+
+ // F4 is the best frame since decoding was so slow that F1 is too old.
+ time_controller_.AdvanceTime(kFps30Delay);
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(4)));
+
+ proxy_->InsertFrame(
+ Builder().Id(5).Time(5 * kFps30Rtp).Refs({3, 4}).AsLast().Build());
+ time_controller_.AdvanceTime(kFps30Delay / 2);
+
+ // F5 is not decodable since F4 was decoded, so a timeout is expected.
+ time_controller_.AdvanceTime(TimeDelta::Millis(10));
+ StartNextDecode();
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForFrame));
+ EXPECT_EQ(timeouts(), 1);
+ // TODO(bugs.webrtc.org/13343): This should be 2 dropped frames since frames 1
+ // and 3 were dropped. However, frame_buffer2 does not mark frame 3 as dropped
+ // which is a bug. Uncomment below when that is fixed for frame_buffer2 is
+ // deleted.
+ // EXPECT_EQ(dropped_frames(), 2);
+}
+
+TEST_P(FrameBufferProxyTest, OldTimestampNotDecodable) {
+ StartNextDecodeForceKeyframe();
+
+ proxy_->InsertFrame(Builder().Id(0).Time(kFps30Rtp).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ // Timestamp is before previous frame's.
+ proxy_->InsertFrame(Builder().Id(1).Time(0).AsLast().Build());
+ StartNextDecode();
+ // F1 should be dropped since its timestamp went backwards.
+ EXPECT_TRUE(WaitForFrameOrTimeout(kMaxWaitForFrame));
+ EXPECT_EQ(timeouts(), 1);
+}
+
+TEST_P(FrameBufferProxyTest, NewFrameInsertedWhileWaitingToReleaseFrame) {
+ StartNextDecodeForceKeyframe();
+ // Initial keyframe.
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(
+ Builder().Id(1).Time(kFps30Rtp).Refs({0}).AsLast().Build());
+ StartNextDecode();
+ EXPECT_FALSE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+
+ // Scheduler is waiting to deliver Frame 1 now. Insert Frame 2. Frame 1 should
+ // be delivered still.
+ proxy_->InsertFrame(
+ Builder().Id(2).Time(kFps30Rtp * 2).Refs({0}).AsLast().Build());
+
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(1)));
+}
+
+TEST_P(FrameBufferProxyTest, SameFrameNotScheduledTwice) {
+ // A frame could be scheduled twice if last_frame() arrive out-of-order but
+ // the older frame is old enough to be fast forwarded.
+ //
+ // 1. F2 arrives and is scheduled.
+ // 2. F3 arrives, but scheduling will not change since F2 is next.
+ // 3. F1 arrives late and scheduling is checked since it is before F2. F1
+ // fast-forwarded since it is older.
+ //
+ // F2 is the best frame, but should only be scheduled once, followed by F3.
+ StartNextDecodeForceKeyframe();
+
+ // First keyframe.
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Millis(15)));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ StartNextDecode();
+
+ // Warmup VCMTiming for 30fps.
+ for (int i = 1; i <= 30; ++i) {
+ proxy_->InsertFrame(Builder().Id(i).Time(i * kFps30Rtp).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(i)));
+ StartNextDecode();
+ }
+
+ // F2 arrives and is scheduled.
+ proxy_->InsertFrame(Builder().Id(32).Time(32 * kFps30Rtp).AsLast().Build());
+
+ // F3 arrives before F2 is extracted.
+ time_controller_.AdvanceTime(kFps30Delay);
+ proxy_->InsertFrame(Builder().Id(33).Time(33 * kFps30Rtp).AsLast().Build());
+
+ // F1 arrives and is fast-forwarded since it is too late.
+ // F2 is already scheduled and should not be rescheduled.
+ time_controller_.AdvanceTime(kFps30Delay / 2);
+ proxy_->InsertFrame(Builder().Id(31).Time(31 * kFps30Rtp).AsLast().Build());
+
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(32)));
+ StartNextDecode();
+
+ EXPECT_TRUE(WaitForFrameOrTimeout(kFps30Delay));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(33)));
+ EXPECT_EQ(dropped_frames(), 1);
+}
+
+TEST_P(FrameBufferProxyTest, TestStatsCallback) {
+ EXPECT_CALL(stats_callback_,
+ OnCompleteFrame(true, kFrameSize, VideoContentType::UNSPECIFIED));
+ EXPECT_CALL(stats_callback_, OnFrameBufferTimingsUpdated);
+
+ // Fake timing having received decoded frame.
+ timing_.StopDecodeTimer(clock_->TimeInMicroseconds() + 1,
+ clock_->TimeInMilliseconds());
+ StartNextDecodeForceKeyframe();
+ proxy_->InsertFrame(Builder().Id(0).Time(0).AsLast().Build());
+ EXPECT_TRUE(WaitForFrameOrTimeout(TimeDelta::Zero()));
+ EXPECT_THAT(last_frame(), Pointee(FrameWithId(0)));
+
+ // Flush stats posted on the decode queue.
+ time_controller_.AdvanceTime(TimeDelta::Zero());
+}
+
+INSTANTIATE_TEST_SUITE_P(FrameBufferProxy,
+ FrameBufferProxyTest,
+ ::testing::Values("WebRTC-FrameBuffer3/Disabled/"));
+
+} // namespace webrtc