diff --git a/test/BUILD.gn b/test/BUILD.gn index 58f0760bdf..940232f242 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -380,6 +380,23 @@ rtc_source_set("test_support") { ] } +rtc_library("fixed_fps_video_frame_writer_adapter") { + visibility = [ "*" ] + testonly = true + sources = [ + "testsupport/fixed_fps_video_frame_writer_adapter.cc", + "testsupport/fixed_fps_video_frame_writer_adapter.h", + ] + deps = [ + ":video_test_support", + "../api/units:time_delta", + "../api/video:video_frame", + "../rtc_base:checks", + "../system_wrappers", + ] + absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] +} + rtc_library("video_test_support") { testonly = true @@ -530,6 +547,22 @@ if (rtc_include_tests && !build_with_chromium) { } } + rtc_library("fixed_fps_video_frame_writer_adapter_test") { + testonly = true + sources = [ "testsupport/fixed_fps_video_frame_writer_adapter_test.cc" ] + deps = [ + ":fixed_fps_video_frame_writer_adapter", + ":test_support", + ":video_test_support", + "../api/units:time_delta", + "../api/units:timestamp", + "../api/video:video_frame", + "../rtc_base/synchronization:mutex", + "time_controller", + ] + absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] + } + rtc_test("test_support_unittests") { deps = [ ":call_config_utils", @@ -538,6 +571,7 @@ if (rtc_include_tests && !build_with_chromium) { ":fake_video_codecs", ":fileutils", ":fileutils_unittests", + ":fixed_fps_video_frame_writer_adapter_test", ":frame_generator_impl", ":perf_test", ":rtc_expect_death", diff --git a/test/testsupport/fixed_fps_video_frame_writer_adapter.cc b/test/testsupport/fixed_fps_video_frame_writer_adapter.cc new file mode 100644 index 0000000000..531dade0e8 --- /dev/null +++ b/test/testsupport/fixed_fps_video_frame_writer_adapter.cc @@ -0,0 +1,114 @@ +/* + * 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 "test/testsupport/fixed_fps_video_frame_writer_adapter.h" + +#include +#include + +#include "absl/types/optional.h" +#include "api/units/time_delta.h" +#include "api/video/video_sink_interface.h" +#include "rtc_base/checks.h" +#include "test/testsupport/video_frame_writer.h" + +namespace webrtc { +namespace test { +namespace { + +constexpr TimeDelta kOneSecond = TimeDelta::Seconds(1); + +} // namespace + +FixedFpsVideoFrameWriterAdapter::FixedFpsVideoFrameWriterAdapter( + int fps, + Clock* clock, + std::unique_ptr delegate) + : inter_frame_interval_(kOneSecond / fps), + clock_(clock), + delegate_(std::move(delegate)) {} + +FixedFpsVideoFrameWriterAdapter::~FixedFpsVideoFrameWriterAdapter() { + Close(); +} + +void FixedFpsVideoFrameWriterAdapter::Close() { + if (is_closed_) { + return; + } + is_closed_ = true; + if (!last_frame_.has_value()) { + return; + } + Timestamp now = Now(); + RTC_CHECK(WriteMissedSlotsExceptLast(now)); + RTC_CHECK(delegate_->WriteFrame(*last_frame_)); + delegate_->Close(); +} + +bool FixedFpsVideoFrameWriterAdapter::WriteFrame(const VideoFrame& frame) { + RTC_CHECK(!is_closed_); + Timestamp now = Now(); + if (!last_frame_.has_value()) { + RTC_CHECK(!last_frame_time_.IsFinite()); + last_frame_ = frame; + last_frame_time_ = now; + return true; + } + + RTC_CHECK(last_frame_time_.IsFinite()); + + if (last_frame_time_ > now) { + // New frame was recevied before expected time "slot" for current + // `last_frame_` came => just replace current `last_frame_` with + // received `frame`. + RTC_CHECK_LE(last_frame_time_ - now, inter_frame_interval_ / 2); + last_frame_ = frame; + return true; + } + + if (!WriteMissedSlotsExceptLast(now)) { + return false; + } + + if (now - last_frame_time_ < inter_frame_interval_ / 2) { + // New frame was received closer to the expected time "slot" for current + // `last_frame_` than to the next "slot" => just replace current + // `last_frame_` with received `frame`. + last_frame_ = frame; + return true; + } + + if (!delegate_->WriteFrame(*last_frame_)) { + return false; + } + last_frame_ = frame; + last_frame_time_ = last_frame_time_ + inter_frame_interval_; + return true; +} + +bool FixedFpsVideoFrameWriterAdapter::WriteMissedSlotsExceptLast( + Timestamp now) { + RTC_CHECK(last_frame_time_.IsFinite()); + while (now - last_frame_time_ > inter_frame_interval_) { + if (!delegate_->WriteFrame(*last_frame_)) { + return false; + } + last_frame_time_ = last_frame_time_ + inter_frame_interval_; + } + return true; +} + +Timestamp FixedFpsVideoFrameWriterAdapter::Now() const { + return clock_->CurrentTime(); +} + +} // namespace test +} // namespace webrtc diff --git a/test/testsupport/fixed_fps_video_frame_writer_adapter.h b/test/testsupport/fixed_fps_video_frame_writer_adapter.h new file mode 100644 index 0000000000..0bdd6ec302 --- /dev/null +++ b/test/testsupport/fixed_fps_video_frame_writer_adapter.h @@ -0,0 +1,86 @@ +/* + * 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. + */ + +#ifndef TEST_TESTSUPPORT_FIXED_FPS_VIDEO_FRAME_WRITER_ADAPTER_H_ +#define TEST_TESTSUPPORT_FIXED_FPS_VIDEO_FRAME_WRITER_ADAPTER_H_ + +#include + +#include "absl/types/optional.h" +#include "api/video/video_sink_interface.h" +#include "system_wrappers/include/clock.h" +#include "test/testsupport/video_frame_writer.h" + +namespace webrtc { +namespace test { + +// Writes video to the specified video writer with specified fixed frame rate. +// If at the point in time X no new frames are passed to the writer, the +// previous frame is used to fill the gap and preserve frame rate. +// +// This adaptor uses next algorithm: +// There are output "slots" at a fixed frame rate (starting at the time of the +// first received frame). Each incoming frame is assigned to the closest output +// slot. Then empty slots are filled by repeating the closest filled slot before +// empty one. If there are multiple frames closest to the same slot, the latest +// received one is used. +// +// The frames are outputted for the whole duration of the class life after the +// first frame was written or until it will be closed. +// +// For example if frames from A to F were received, then next output sequence +// will be generated: +// Received frames: A B C D EF Destructor called +// | | | | || | +// v v v v vv v +// X----X----X----X----X----X----X----X----X----+----+-- +// | | | | | | | | | +// Produced frames: A A A B C C F F F +// +// This class is not thread safe. +class FixedFpsVideoFrameWriterAdapter : public VideoFrameWriter { + public: + FixedFpsVideoFrameWriterAdapter(int fps, + Clock* clock, + std::unique_ptr delegate); + ~FixedFpsVideoFrameWriterAdapter() override; + + bool WriteFrame(const webrtc::VideoFrame& frame) override; + + // Closes adapter and underlying delegate. User mustn't call to the WriteFrame + // after calling this method. + void Close() override; + + private: + // Writes `last_frame_` for each "slot" from `last_frame_time_` up to now + // excluding the last one. + // Updates `last_frame_time_` to the position of the last NOT WRITTEN frame. + // Returns true if all writes were successful, otherwise retuns false. In such + // case it is not guranteed how many frames were actually written. + bool WriteMissedSlotsExceptLast(Timestamp now); + Timestamp Now() const; + + // Because `TimeDelta` stores time with microseconds precision + // `last_frame_time_` may have a small drift and for very long streams it + // must be updated to use double for time. + const TimeDelta inter_frame_interval_; + Clock* const clock_; + std::unique_ptr delegate_; + bool is_closed_ = false; + + // Expected time slot for the last frame. + Timestamp last_frame_time_ = Timestamp::MinusInfinity(); + absl::optional last_frame_ = absl::nullopt; +}; + +} // namespace test +} // namespace webrtc + +#endif // TEST_TESTSUPPORT_FIXED_FPS_VIDEO_FRAME_WRITER_ADAPTER_H_ diff --git a/test/testsupport/fixed_fps_video_frame_writer_adapter_test.cc b/test/testsupport/fixed_fps_video_frame_writer_adapter_test.cc new file mode 100644 index 0000000000..5ee4701cc9 --- /dev/null +++ b/test/testsupport/fixed_fps_video_frame_writer_adapter_test.cc @@ -0,0 +1,320 @@ +/* + * 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 "test/testsupport/fixed_fps_video_frame_writer_adapter.h" + +#include +#include +#include + +#include "api/units/time_delta.h" +#include "api/units/timestamp.h" +#include "api/video/i420_buffer.h" +#include "api/video/video_frame.h" +#include "rtc_base/synchronization/mutex.h" +#include "test/gmock.h" +#include "test/gtest.h" +#include "test/testsupport/video_frame_writer.h" +#include "test/time_controller/simulated_time_controller.h" + +namespace webrtc { +namespace test { +namespace { + +constexpr TimeDelta kOneSecond = TimeDelta::Seconds(1); + +using ::testing::ElementsAre; + +class InMemoryVideoWriter : public VideoFrameWriter { + public: + ~InMemoryVideoWriter() override = default; + + bool WriteFrame(const webrtc::VideoFrame& frame) override { + MutexLock lock(&mutex_); + received_frames_.push_back(frame); + return true; + } + + void Close() override {} + + std::vector received_frames() const { + MutexLock lock(&mutex_); + return received_frames_; + } + + private: + mutable Mutex mutex_; + std::vector received_frames_ RTC_GUARDED_BY(mutex_); +}; + +VideoFrame EmptyFrameWithId(uint16_t frame_id) { + return VideoFrame::Builder() + .set_video_frame_buffer(I420Buffer::Create(1, 1)) + .set_id(frame_id) + .build(); +} + +std::vector FrameIds(const std::vector& frames) { + std::vector out; + for (const VideoFrame& frame : frames) { + out.push_back(frame.id()); + } + return out; +} + +std::unique_ptr CreateSimulatedTimeController() { + // Using an offset of 100000 to get nice fixed width and readable + // timestamps in typical test scenarios. + const Timestamp kSimulatedStartTime = Timestamp::Seconds(100000); + return std::make_unique(kSimulatedStartTime); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + WhenWrittenWithSameFpsVideoIsCorrect) { + auto time_controller = CreateSimulatedTimeController(); + int fps = 25; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer(fps, time_controller->GetClock(), + std::move(inmemory_writer)); + + for (int i = 1; i <= 30; ++i) { + video_writer.WriteFrame(EmptyFrameWithId(i)); + time_controller->AdvanceTime(kOneSecond / fps); + } + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT( + FrameIds(received_frames), + ElementsAre(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, FrameIsRepeatedWhenThereIsAFreeze) { + auto time_controller = CreateSimulatedTimeController(); + int fps = 25; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer(fps, time_controller->GetClock(), + std::move(inmemory_writer)); + + // Write 10 frames + for (int i = 1; i <= 10; ++i) { + video_writer.WriteFrame(EmptyFrameWithId(i)); + time_controller->AdvanceTime(kOneSecond / fps); + } + + // Freeze for 4 frames + time_controller->AdvanceTime(4 * kOneSecond / fps); + + // Write 10 more frames + for (int i = 11; i <= 20; ++i) { + video_writer.WriteFrame(EmptyFrameWithId(i)); + time_controller->AdvanceTime(kOneSecond / fps); + } + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), + ElementsAre(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, NoFramesWritten) { + auto time_controller = CreateSimulatedTimeController(); + int fps = 25; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer(fps, time_controller->GetClock(), + std::move(inmemory_writer)); + time_controller->AdvanceTime(TimeDelta::Millis(100)); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + ASSERT_TRUE(received_frames.empty()); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + FreezeInTheMiddleAndNewFrameReceivedBeforeMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(2.3 * kInterval); + video_writer.WriteFrame(EmptyFrameWithId(2)); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1, 1, 2)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + FreezeInTheMiddleAndNewFrameReceivedAfterMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(2.5 * kInterval); + video_writer.WriteFrame(EmptyFrameWithId(2)); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1, 1, 1, 2)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + NewFrameReceivedBeforeMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(0.3 * kInterval); + video_writer.WriteFrame(EmptyFrameWithId(2)); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(2)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + NewFrameReceivedAfterMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(0.5 * kInterval); + video_writer.WriteFrame(EmptyFrameWithId(2)); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1, 2)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + FreeezeAtTheEndAndDestroyBeforeMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(2.3 * kInterval); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1, 1, 1)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + FreeezeAtTheEndAndDestroyAfterMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(2.5 * kInterval); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1, 1, 1)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + DestroyBeforeMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(0.3 * kInterval); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1)); +} + +TEST(FixedFpsVideoFrameWriterAdapterTest, + DestroyAfterMiddleOfExpectedInterval) { + auto time_controller = CreateSimulatedTimeController(); + constexpr int kFps = 10; + constexpr TimeDelta kInterval = kOneSecond / kFps; + + auto inmemory_writer = std::make_unique(); + InMemoryVideoWriter* inmemory_writer_ref = inmemory_writer.get(); + + FixedFpsVideoFrameWriterAdapter video_writer( + kFps, time_controller->GetClock(), std::move(inmemory_writer)); + video_writer.WriteFrame(EmptyFrameWithId(1)); + time_controller->AdvanceTime(0.5 * kInterval); + video_writer.Close(); + + std::vector received_frames = + inmemory_writer_ref->received_frames(); + EXPECT_THAT(FrameIds(received_frames), ElementsAre(1)); +} + +} // namespace +} // namespace test +} // namespace webrtc