| /* |
| * 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/decode_synchronizer.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "absl/functional/any_invocable.h" |
| #include "api/metronome/test/fake_metronome.h" |
| #include "api/units/time_delta.h" |
| #include "test/gmock.h" |
| #include "test/gtest.h" |
| #include "test/time_controller/simulated_time_controller.h" |
| #include "video/frame_decode_scheduler.h" |
| #include "video/frame_decode_timing.h" |
| |
| using ::testing::_; |
| using ::testing::Eq; |
| using ::testing::Invoke; |
| using ::testing::Mock; |
| using ::testing::MockFunction; |
| using ::testing::Return; |
| |
| namespace webrtc { |
| |
| class MockMetronome : public Metronome { |
| public: |
| MOCK_METHOD(void, |
| RequestCallOnNextTick, |
| (absl::AnyInvocable<void() &&> callback), |
| (override)); |
| MOCK_METHOD(TimeDelta, TickPeriod, (), (const override)); |
| }; |
| |
| class DecodeSynchronizerTest : public ::testing::Test { |
| public: |
| static constexpr TimeDelta kTickPeriod = TimeDelta::Millis(33); |
| |
| DecodeSynchronizerTest() |
| : time_controller_(Timestamp::Millis(1337)), |
| clock_(time_controller_.GetClock()), |
| metronome_(kTickPeriod), |
| decode_synchronizer_(clock_, |
| &metronome_, |
| time_controller_.GetMainThread()) {} |
| |
| protected: |
| GlobalSimulatedTimeController time_controller_; |
| Clock* clock_; |
| test::ForcedTickMetronome metronome_; |
| DecodeSynchronizer decode_synchronizer_; |
| }; |
| |
| TEST_F(DecodeSynchronizerTest, AllFramesReadyBeforeNextTickDecoded) { |
| MockFunction<void(uint32_t, Timestamp)> mock_callback1; |
| auto scheduler1 = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| MockFunction<void(unsigned int, Timestamp)> mock_callback2; |
| auto scheduler2 = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| { |
| uint32_t frame_rtp = 90000; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = |
| clock_->CurrentTime() + kTickPeriod - TimeDelta::Millis(3), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(60)}; |
| scheduler1->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback1.AsStdFunction()); |
| EXPECT_CALL(mock_callback1, |
| Call(Eq(frame_rtp), Eq(frame_sched.render_time))); |
| } |
| { |
| uint32_t frame_rtp = 123456; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = |
| clock_->CurrentTime() + kTickPeriod - TimeDelta::Millis(2), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(70)}; |
| scheduler2->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback2.AsStdFunction()); |
| EXPECT_CALL(mock_callback2, |
| Call(Eq(frame_rtp), Eq(frame_sched.render_time))); |
| } |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // Cleanup |
| scheduler1->Stop(); |
| scheduler2->Stop(); |
| } |
| |
| TEST_F(DecodeSynchronizerTest, FramesNotDecodedIfDecodeTimeIsInNextInterval) { |
| MockFunction<void(unsigned int, Timestamp)> mock_callback; |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| uint32_t frame_rtp = 90000; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = |
| clock_->CurrentTime() + kTickPeriod + TimeDelta::Millis(10), |
| .render_time = |
| clock_->CurrentTime() + kTickPeriod + TimeDelta::Millis(30)}; |
| scheduler->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback.AsStdFunction()); |
| |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| // No decodes should have happened in this tick. |
| ::testing::Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| // Decode should happen on next tick. |
| EXPECT_CALL(mock_callback, Call(Eq(frame_rtp), Eq(frame_sched.render_time))); |
| time_controller_.AdvanceTime(kTickPeriod); |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // Cleanup |
| scheduler->Stop(); |
| } |
| |
| TEST_F(DecodeSynchronizerTest, FrameDecodedOnce) { |
| MockFunction<void(unsigned int, Timestamp)> mock_callback; |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| uint32_t frame_rtp = 90000; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = clock_->CurrentTime() + TimeDelta::Millis(30), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(60)}; |
| scheduler->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback.AsStdFunction()); |
| EXPECT_CALL(mock_callback, Call(_, _)).Times(1); |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| // Trigger tick again. No frame should be decoded now. |
| time_controller_.AdvanceTime(kTickPeriod); |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // Cleanup |
| scheduler->Stop(); |
| } |
| |
| TEST_F(DecodeSynchronizerTest, FrameWithDecodeTimeInPastDecodedImmediately) { |
| MockFunction<void(unsigned int, Timestamp)> mock_callback; |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| uint32_t frame_rtp = 90000; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = clock_->CurrentTime() - TimeDelta::Millis(5), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(30)}; |
| EXPECT_CALL(mock_callback, Call(Eq(90000u), _)).Times(1); |
| scheduler->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback.AsStdFunction()); |
| // Verify the callback was invoked already. |
| ::testing::Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // Cleanup |
| scheduler->Stop(); |
| } |
| |
| TEST_F(DecodeSynchronizerTest, |
| FrameWithDecodeTimeFarBeforeNextTickDecodedImmediately) { |
| MockFunction<void(unsigned int, Timestamp)> mock_callback; |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| // Frame which would be behind by more than kMaxAllowedFrameDelay after |
| // the next tick. |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = clock_->CurrentTime() + kTickPeriod - |
| FrameDecodeTiming::kMaxAllowedFrameDelay - |
| TimeDelta::Millis(1), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(30)}; |
| EXPECT_CALL(mock_callback, Call(Eq(90000u), _)).Times(1); |
| scheduler->ScheduleFrame(90000, frame_sched, mock_callback.AsStdFunction()); |
| // Verify the callback was invoked already. |
| ::testing::Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| time_controller_.AdvanceTime(kTickPeriod); |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // A frame that would be behind by exactly kMaxAllowedFrameDelay after next |
| // tick should decode at the next tick. |
| FrameDecodeTiming::FrameSchedule queued_frame{ |
| .latest_decode_time = clock_->CurrentTime() + kTickPeriod - |
| FrameDecodeTiming::kMaxAllowedFrameDelay, |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(30)}; |
| scheduler->ScheduleFrame(180000, queued_frame, mock_callback.AsStdFunction()); |
| // Verify the callback was invoked already. |
| ::testing::Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| EXPECT_CALL(mock_callback, Call(Eq(180000u), _)).Times(1); |
| time_controller_.AdvanceTime(kTickPeriod); |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| |
| // Cleanup |
| scheduler->Stop(); |
| } |
| |
| TEST_F(DecodeSynchronizerTest, FramesNotReleasedAfterStop) { |
| MockFunction<void(unsigned int, Timestamp)> mock_callback; |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| |
| uint32_t frame_rtp = 90000; |
| FrameDecodeTiming::FrameSchedule frame_sched{ |
| .latest_decode_time = clock_->CurrentTime() + TimeDelta::Millis(30), |
| .render_time = clock_->CurrentTime() + TimeDelta::Millis(60)}; |
| scheduler->ScheduleFrame(frame_rtp, frame_sched, |
| mock_callback.AsStdFunction()); |
| // Cleanup |
| scheduler->Stop(); |
| |
| // No callback should occur on this tick since Stop() was called before. |
| metronome_.Tick(); |
| time_controller_.AdvanceTime(TimeDelta::Zero()); |
| } |
| |
| TEST(DecodeSynchronizerStandaloneTest, |
| MetronomeNotListenedWhenNoStreamsAreActive) { |
| GlobalSimulatedTimeController time_controller(Timestamp::Millis(4711)); |
| Clock* clock(time_controller.GetClock()); |
| MockMetronome metronome; |
| ON_CALL(metronome, TickPeriod).WillByDefault(Return(TimeDelta::Seconds(1))); |
| DecodeSynchronizer decode_synchronizer_(clock, &metronome, |
| time_controller.GetMainThread()); |
| absl::AnyInvocable<void() &&> callback; |
| EXPECT_CALL(metronome, RequestCallOnNextTick) |
| .WillOnce(Invoke([&callback](absl::AnyInvocable<void() &&> cb) { |
| callback = std::move(cb); |
| })); |
| auto scheduler = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| auto scheduler2 = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| scheduler->Stop(); |
| scheduler2->Stop(); |
| time_controller.AdvanceTime(TimeDelta::Seconds(1)); |
| ASSERT_TRUE(callback); |
| (std::move)(callback)(); |
| } |
| |
| TEST(DecodeSynchronizerStandaloneTest, |
| RegistersCallbackOnceDuringRepeatedRegistrations) { |
| GlobalSimulatedTimeController time_controller(Timestamp::Millis(4711)); |
| Clock* clock(time_controller.GetClock()); |
| MockMetronome metronome; |
| ON_CALL(metronome, TickPeriod).WillByDefault(Return(TimeDelta::Seconds(1))); |
| DecodeSynchronizer decode_synchronizer_(clock, &metronome, |
| time_controller.GetMainThread()); |
| // Expect at most 1 call to register a callback. |
| EXPECT_CALL(metronome, RequestCallOnNextTick); |
| auto scheduler1 = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| scheduler1->Stop(); |
| auto scheduler2 = decode_synchronizer_.CreateSynchronizedFrameScheduler(); |
| Mock::VerifyAndClearExpectations(&metronome); |
| scheduler2->Stop(); |
| } |
| |
| } // namespace webrtc |