Add FrameBufferProxy test for low-latency renderer
Ensures that frames are decoded instantly when in low-latency render
mode. This also tests the max queue size behaviour. Adds a new test
suite for FrameBufferProxy that sets the appropriate field trials.
* Fixes FrameDecodeTiming to never use negative wait times for decode
timestamps.
R=kron@webrtc.org
Change-Id: I06cbec52e1e866e21aa964b24c4fd0163c26961b
Bug: webrtc:13658
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/251601
Reviewed-by: Erik Språng <sprang@webrtc.org>
Reviewed-by: Johannes Kron <kron@webrtc.org>
Commit-Queue: Evan Shrubsole <eshr@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35999}
diff --git a/modules/video_coding/timing.cc b/modules/video_coding/timing.cc
index 99e525a..df98416 100644
--- a/modules/video_coding/timing.cc
+++ b/modules/video_coding/timing.cc
@@ -10,7 +10,6 @@
#include "modules/video_coding/timing.h"
-
#include <algorithm>
#include "rtc_base/experiments/field_trial_parser.h"
diff --git a/video/BUILD.gn b/video/BUILD.gn
index 1a247f4..caa4f6d 100644
--- a/video/BUILD.gn
+++ b/video/BUILD.gn
@@ -362,6 +362,7 @@
]
deps = [
"../api/task_queue",
+ "../api/units:time_delta",
"../modules/video_coding:timing",
"../rtc_base:logging",
"../rtc_base/task_utils:pending_task_safety_flag",
diff --git a/video/frame_buffer_proxy.cc b/video/frame_buffer_proxy.cc
index d294eeb..a2e5406 100644
--- a/video/frame_buffer_proxy.cc
+++ b/video/frame_buffer_proxy.cc
@@ -140,7 +140,7 @@
// Encapsulates use of the new frame buffer for use in VideoReceiveStream. This
// behaves the same as the FrameBuffer2Proxy but uses frame_buffer3 instead.
-// Responsiblities from frame_buffer2, like stats, jitter and frame timing
+// Responsibilities from frame_buffer2, like stats, jitter and frame timing
// accounting are moved into this pro
class FrameBuffer3Proxy : public FrameBufferProxy {
public:
@@ -323,6 +323,8 @@
std::unique_ptr<EncodedFrame> frame =
CombineAndDeleteFrames(std::move(frames));
+ timing_->SetLastDecodeScheduledTimestamp(now_ms);
+
decoder_ready_for_new_frame_ = false;
// VideoReceiveStream2 wants frames on the decoder thread.
decode_queue_->PostTask(ToQueuedTask(
diff --git a/video/frame_buffer_proxy_unittest.cc b/video/frame_buffer_proxy_unittest.cc
index fc266d2..073d30b 100644
--- a/video/frame_buffer_proxy_unittest.cc
+++ b/video/frame_buffer_proxy_unittest.cc
@@ -25,6 +25,7 @@
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "api/video/video_content_type.h"
+#include "api/video/video_timing.h"
#include "rtc_base/checks.h"
#include "rtc_base/event.h"
#include "system_wrappers/include/field_trial.h"
@@ -201,10 +202,11 @@
constexpr auto kMaxWaitForKeyframe = TimeDelta::Millis(500);
constexpr auto kMaxWaitForFrame = TimeDelta::Millis(1500);
-class FrameBufferProxyTest : public ::testing::TestWithParam<std::string>,
- public FrameSchedulingReceiver {
+class FrameBufferProxyFixture
+ : public ::testing::WithParamInterface<std::string>,
+ public FrameSchedulingReceiver {
public:
- FrameBufferProxyTest()
+ FrameBufferProxyFixture()
: field_trials_(GetParam()),
time_controller_(kClockStart),
clock_(time_controller_.GetClock()),
@@ -232,7 +234,7 @@
[this](auto num_dropped) { dropped_frames_ += num_dropped; });
}
- ~FrameBufferProxyTest() override {
+ ~FrameBufferProxyFixture() override {
if (proxy_) {
proxy_->StopOnWorker();
}
@@ -320,6 +322,9 @@
absl::optional<WaitResult> wait_result_;
};
+class FrameBufferProxyTest : public ::testing::Test,
+ public FrameBufferProxyFixture {};
+
TEST_P(FrameBufferProxyTest, InitialTimeoutAfterKeyframeTimeoutPeriod) {
StartNextDecodeForceKeyframe();
// No frame insterted. Timeout expected.
@@ -700,4 +705,88 @@
"WebRTC-FrameBuffer3/arm:FrameBuffer3/",
"WebRTC-FrameBuffer3/arm:SyncDecoding/"));
+class LowLatencyFrameBufferProxyTest : public ::testing::Test,
+ public FrameBufferProxyFixture {};
+
+TEST_P(LowLatencyFrameBufferProxyTest,
+ FramesDecodedInstantlyWithLowLatencyRendering) {
+ // Initial keyframe.
+ StartNextDecodeForceKeyframe();
+ timing_.set_min_playout_delay(0);
+ timing_.set_max_playout_delay(10);
+ auto frame = Builder().Id(0).Time(0).AsLast().Build();
+ // Playout delay of 0 implies low-latency rendering.
+ frame->SetPlayoutDelay({0, 10});
+ proxy_->InsertFrame(std::move(frame));
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(0)));
+
+ // Delta frame would normally wait here, but should decode at the pacing rate
+ // in low-latency mode.
+ StartNextDecode();
+ frame = Builder().Id(1).Time(kFps30Rtp).AsLast().Build();
+ frame->SetPlayoutDelay({0, 10});
+ proxy_->InsertFrame(std::move(frame));
+ // Pacing is set to 16ms in the field trial so we should not decode yet.
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Eq(absl::nullopt));
+ time_controller_.AdvanceTime(TimeDelta::Millis(16));
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(1)));
+}
+
+TEST_P(LowLatencyFrameBufferProxyTest, ZeroPlayoutDelayFullQueue) {
+ // Initial keyframe.
+ StartNextDecodeForceKeyframe();
+ timing_.set_min_playout_delay(0);
+ timing_.set_max_playout_delay(10);
+ auto frame = Builder().Id(0).Time(0).AsLast().Build();
+ // Playout delay of 0 implies low-latency rendering.
+ frame->SetPlayoutDelay({0, 10});
+ proxy_->InsertFrame(std::move(frame));
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(0)));
+
+ // Queue up 5 frames (configured max queue size for 0-playout delay pacing).
+ for (int id = 1; id <= 6; ++id) {
+ frame = Builder().Id(id).Time(kFps30Rtp * id).AsLast().Build();
+ frame->SetPlayoutDelay({0, 10});
+ proxy_->InsertFrame(std::move(frame));
+ }
+
+ // The queue is at its max size for zero playout delay pacing, so the pacing
+ // should be ignored and the next frame should be decoded instantly.
+ StartNextDecode();
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(1)));
+}
+
+TEST_P(LowLatencyFrameBufferProxyTest, MinMaxDelayZeroLowLatencyMode) {
+ // Initial keyframe.
+ StartNextDecodeForceKeyframe();
+ timing_.set_min_playout_delay(0);
+ timing_.set_max_playout_delay(0);
+ auto frame = Builder().Id(0).Time(0).AsLast().Build();
+ // Playout delay of 0 implies low-latency rendering.
+ frame->SetPlayoutDelay({0, 0});
+ proxy_->InsertFrame(std::move(frame));
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(0)));
+
+ // Delta frame would normally wait here, but should decode at the pacing rate
+ // in low-latency mode.
+ StartNextDecode();
+ frame = Builder().Id(1).Time(kFps30Rtp).AsLast().Build();
+ frame->SetPlayoutDelay({0, 0});
+ proxy_->InsertFrame(std::move(frame));
+ // The min/max=0 version of low-latency rendering will result in a large
+ // negative decode wait time, so the frame should be ready right away.
+ EXPECT_THAT(WaitForFrameOrTimeout(TimeDelta::Zero()), Frame(WithId(1)));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FrameBufferProxy,
+ LowLatencyFrameBufferProxyTest,
+ ::testing::Values(
+ "WebRTC-FrameBuffer3/arm:FrameBuffer2/"
+ "WebRTC-ZeroPlayoutDelay/min_pacing:16ms,max_decode_queue_size:5/",
+ "WebRTC-FrameBuffer3/arm:FrameBuffer3/"
+ "WebRTC-ZeroPlayoutDelay/min_pacing:16ms,max_decode_queue_size:5/",
+ "WebRTC-FrameBuffer3/arm:SyncDecoding/"
+ "WebRTC-ZeroPlayoutDelay/min_pacing:16ms,max_decode_queue_size:5/"));
+
} // namespace webrtc
diff --git a/video/frame_decode_timing.cc b/video/frame_decode_timing.cc
index 02567ba..ddc6030 100644
--- a/video/frame_decode_timing.cc
+++ b/video/frame_decode_timing.cc
@@ -10,7 +10,10 @@
#include "video/frame_decode_timing.h"
+#include <algorithm>
+
#include "absl/types/optional.h"
+#include "api/units/time_delta.h"
#include "rtc_base/logging.h"
namespace webrtc {
@@ -45,7 +48,9 @@
RTC_DLOG(LS_VERBOSE) << "Selected frame with rtp " << next_temporal_unit_rtp
<< " render time " << render_time.ms()
<< " with a max wait of " << max_wait.ms() << "ms";
- return FrameSchedule{.latest_decode_time = now + max_wait,
+
+ Timestamp latest_decode_time = now + std::max(max_wait, TimeDelta::Zero());
+ return FrameSchedule{.latest_decode_time = latest_decode_time,
.render_time = render_time};
}
diff --git a/video/frame_decode_timing_unittest.cc b/video/frame_decode_timing_unittest.cc
index 1932e85..ac5bf9e 100644
--- a/video/frame_decode_timing_unittest.cc
+++ b/video/frame_decode_timing_unittest.cc
@@ -89,7 +89,8 @@
}
TEST_F(FrameDecodeTimingTest, FastForwardsFrameTooFarInThePast) {
- const TimeDelta decode_delay = TimeDelta::Millis(-6);
+ const TimeDelta decode_delay =
+ -FrameDecodeTiming::kMaxAllowedFrameDelay - TimeDelta::Millis(1);
const Timestamp render_time = clock_.CurrentTime();
timing_.SetTimes(90000, render_time, decode_delay);
@@ -99,14 +100,16 @@
}
TEST_F(FrameDecodeTimingTest, NoFastForwardIfOnlyFrameToDecode) {
- const TimeDelta decode_delay = TimeDelta::Millis(-6);
+ const TimeDelta decode_delay =
+ -FrameDecodeTiming::kMaxAllowedFrameDelay - TimeDelta::Millis(1);
const Timestamp render_time = clock_.CurrentTime();
timing_.SetTimes(90000, render_time, decode_delay);
+ // Negative `decode_delay` means that `latest_decode_time` is now.
EXPECT_THAT(frame_decode_scheduler_.OnFrameBufferUpdated(90000, 90000, false),
Optional(AllOf(
Field(&FrameDecodeTiming::FrameSchedule::latest_decode_time,
- Eq(clock_.CurrentTime() + decode_delay)),
+ Eq(clock_.CurrentTime())),
Field(&FrameDecodeTiming::FrameSchedule::render_time,
Eq(render_time)))));
}