blob: 237d7deb49d7e54199896c5e4f98103d5618b590 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <queue>
#include "media/base/video_frame_pool.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/modules/mediastream/low_latency_video_renderer_algorithm.h"
namespace blink {
class LowLatencyVideoRendererAlgorithmTest : public testing::Test {
public:
LowLatencyVideoRendererAlgorithmTest()
: algorithm_(nullptr),
current_render_time_(base::TimeTicks() + base::TimeDelta::FromDays(1)) {
}
~LowLatencyVideoRendererAlgorithmTest() override = default;
scoped_refptr<media::VideoFrame> CreateFrame(
size_t maximum_composition_delay_in_frames) {
const gfx::Size natural_size(8, 8);
scoped_refptr<media::VideoFrame> frame = frame_pool_.CreateFrame(
media::PIXEL_FORMAT_I420, natural_size, gfx::Rect(natural_size),
natural_size, base::TimeDelta());
frame->metadata().maximum_composition_delay_in_frames =
maximum_composition_delay_in_frames;
return frame;
}
int CreateAndEnqueueFrame(int max_composition_delay_in_frames) {
scoped_refptr<media::VideoFrame> frame =
CreateFrame(max_composition_delay_in_frames);
int unique_id = frame->unique_id();
algorithm_.EnqueueFrame(std::move(frame));
return unique_id;
}
size_t frames_queued() const { return algorithm_.frames_queued(); }
scoped_refptr<media::VideoFrame> RenderAndStep(size_t* frames_dropped) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 60.0); // 60fps.
return RenderAndStep(frames_dropped, kRenderInterval);
}
scoped_refptr<media::VideoFrame> RenderAndStep(
size_t* frames_dropped,
base::TimeDelta render_interval) {
const base::TimeTicks start = current_render_time_;
current_render_time_ += render_interval;
const base::TimeTicks end = current_render_time_;
return algorithm_.Render(start, end, frames_dropped);
}
void StepUntilJustBeforeNextFrameIsRendered(
base::TimeDelta render_interval,
base::Optional<int> expected_id = base::nullopt) {
// No frame will be rendered until the total render time that has passed is
// greater than the frame duration of a frame.
base::TimeTicks start_time = current_render_time_;
while (current_render_time_ - start_time + render_interval <
FrameDuration()) {
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(nullptr, render_interval);
if (expected_id) {
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), *expected_id);
} else {
EXPECT_FALSE(rendered_frame);
}
}
}
base::TimeDelta FrameDuration() const {
// Assume 60 Hz video content.
return base::TimeDelta::FromMillisecondsD(1000.0 / 60.0);
}
protected:
media::VideoFramePool frame_pool_;
LowLatencyVideoRendererAlgorithm algorithm_;
base::TimeTicks current_render_time_;
private:
DISALLOW_COPY_AND_ASSIGN(LowLatencyVideoRendererAlgorithmTest);
};
TEST_F(LowLatencyVideoRendererAlgorithmTest, Empty) {
size_t frames_dropped = 0;
EXPECT_EQ(0u, frames_queued());
EXPECT_FALSE(RenderAndStep(&frames_dropped));
EXPECT_EQ(0u, frames_dropped);
EXPECT_EQ(0u, frames_queued());
}
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode60Hz) {
// Every frame rendered.
constexpr int kNumberOfFrames = 100;
constexpr int kMaxCompositionDelayInFrames = 6;
for (int i = 0; i < kNumberOfFrames; ++i) {
int frame_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
size_t frames_dropped = 0u;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), frame_id);
EXPECT_EQ(frames_dropped, 0u);
}
}
// Half frame rate (30Hz playing back 60Hz video)
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode30Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 30.0); // 30Hz.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr size_t kNumberOfFrames = 120;
for (size_t i = 0; i < kNumberOfFrames; ++i) {
scoped_refptr<media::VideoFrame> frame;
size_t expected_frames_dropped = 0;
if (i > 0) {
// This frame will be dropped.
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
++expected_frames_dropped;
}
int last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), last_id);
EXPECT_EQ(frames_dropped, expected_frames_dropped);
}
// Only the currently rendered frame is in the queue.
EXPECT_EQ(frames_queued(), 1u);
}
// Fractional frame rate (90Hz playing back 60Hz video)
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode90Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 90.0); // 90Hz.
constexpr int kMaxCompositionDelayInFrames = 6;
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
constexpr size_t kNumberOfFramesToSubmit = 100;
size_t submitted_frames = 0;
while (submitted_frames < kNumberOfFramesToSubmit) {
// In each while iteration: Enqueue two new frames (60Hz) and render three
// times (90Hz).
for (int i = 0; i < 2; ++i) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
// Enqueue a new frame.
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
++submitted_frames;
}
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
}
}
// Double frame rate (120Hz playing back 60Hz video)
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode120Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 120.0); // 120Hz.
constexpr int kMaxCompositionDelayInFrames = 6;
// Add one initial frame.
int last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
constexpr size_t kNumberOfFrames = 120;
for (size_t i = 0; i < kNumberOfFrames; ++i) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
int rendered_frame_id = last_id;
EXPECT_EQ(rendered_frame->unique_id(), rendered_frame_id);
last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
// The same frame should be rendered.
rendered_frame = RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), rendered_frame_id);
}
// Two frames in the queue including the last rendered frame.
EXPECT_EQ(frames_queued(), 2u);
}
// Super high display rate (600Hz playing back 60Hz video)
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode600Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 600.0 + 1.0e-3); // 600Hz.
constexpr int kMaxCompositionDelayInFrames = 6;
// Add one initial frame.
int last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
constexpr size_t kNumberOfFrames = 120;
for (size_t i = 0; i < kNumberOfFrames; ++i) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
int rendered_frame_id = last_id;
EXPECT_EQ(rendered_frame->unique_id(), rendered_frame_id);
last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
// The same frame should be rendered 9 times.
StepUntilJustBeforeNextFrameIsRendered(kRenderInterval, rendered_frame_id);
}
// Two frames in the queue including the last rendered frame.
EXPECT_EQ(frames_queued(), 2u);
}
TEST_F(LowLatencyVideoRendererAlgorithmTest,
DropAllFramesIfQueueExceedsMaxSize) {
// Create an initial queue of 60 frames.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr size_t kInitialQueueSize = 60;
int last_id = 0;
for (size_t i = 0; i < kInitialQueueSize; ++i) {
last_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
}
EXPECT_EQ(frames_queued(), kInitialQueueSize);
// Last submitted frame should be rendered.
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, kInitialQueueSize - 1);
EXPECT_EQ(rendered_frame->unique_id(), last_id);
}
TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode60Hz) {
// Enter drain mode when more than 6 frames are in the queue.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr int kNumberOfFramesSubmitted = kMaxCompositionDelayInFrames + 1;
std::queue<int> enqueued_frame_ids;
for (int i = 0; i < kNumberOfFramesSubmitted; ++i) {
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
// Every other frame will be rendered until there's one frame in the queue.
int processed_frames_count = 0;
while (processed_frames_count < kNumberOfFramesSubmitted - 1) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 1u);
enqueued_frame_ids.pop();
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
processed_frames_count += 1 + frames_dropped;
}
// One more frame to render.
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
EXPECT_EQ(enqueued_frame_ids.size(), 0u);
}
TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode60Hz) {
// Enter drain mode when more than 6 frames are in the queue.
constexpr int kMaxCompositionDelayInFrames = 6;
int number_of_frames_submitted = kMaxCompositionDelayInFrames + 1;
std::queue<int> enqueued_frame_ids;
for (int i = 0; i < number_of_frames_submitted; ++i) {
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
// Every other frame will be rendered until there's one frame in the queue.
int processed_frames_count = 0;
while (processed_frames_count < number_of_frames_submitted - 1) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 1u);
enqueued_frame_ids.pop();
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
// Enqueue a new frame.
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
++number_of_frames_submitted;
processed_frames_count += 1 + frames_dropped;
}
// Continue in normal mode without dropping frames.
constexpr int kNumberOfFramesInNormalMode = 30;
for (int i = 0; i < kNumberOfFramesInNormalMode; ++i) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
}
// Double Rate Drain (120Hz playing back 60Hz video in DRAIN mode)
TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode120Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 120.0); // 120Hz.
// Enter drain mode when more than 6 frames are in the queue.
constexpr int kMaxCompositionDelayInFrames = 6;
// Process one frame to initialize the algorithm.
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
EXPECT_TRUE(RenderAndStep(nullptr, kRenderInterval));
constexpr int kNumberOfFramesSubmitted = kMaxCompositionDelayInFrames + 1;
std::queue<int> enqueued_frame_ids;
for (int i = 0; i < kNumberOfFramesSubmitted; ++i) {
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
// Every frame will be rendered at double rate until there's one frame in the
// queue.
int processed_frames_count = 0;
while (processed_frames_count < kNumberOfFramesSubmitted - 1) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
processed_frames_count += 1 + frames_dropped;
}
// One more frame to render.
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, 0u);
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
EXPECT_EQ(enqueued_frame_ids.size(), 0u);
}
TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction60Hz) {
// Create an initial queue of 5 frames.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr size_t kInitialQueueSize = 5;
std::queue<int> enqueued_frame_ids;
for (size_t i = 0; i < kInitialQueueSize; ++i) {
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
EXPECT_EQ(frames_queued(), kInitialQueueSize);
constexpr size_t kNumberOfFramesSubmitted = 100;
constexpr int kMinimumNumberOfFramesBetweenDrops = 8;
int processed_frames_since_last_frame_drop = 0;
for (size_t i = kInitialQueueSize; i < kNumberOfFramesSubmitted; ++i) {
// Every frame will be rendered with occasional frame drops to reduce the
// steady state queue.
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped);
ASSERT_TRUE(rendered_frame);
if (frames_dropped > 0) {
ASSERT_EQ(frames_dropped, 1u);
EXPECT_GE(processed_frames_since_last_frame_drop,
kMinimumNumberOfFramesBetweenDrops);
enqueued_frame_ids.pop();
processed_frames_since_last_frame_drop = 0;
} else {
++processed_frames_since_last_frame_drop;
}
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop();
enqueued_frame_ids.push(
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
}
// Steady state queue should now have been reduced to one frame + the current
// frame that is also counted.
EXPECT_EQ(frames_queued(), 2u);
}
// Fractional rate, steady state queue reduction.
TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateReduction90Hz) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 90.0); // 90Hz.
// Create an initial queue of 5 frames.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr size_t kInitialQueueSize = 5;
for (size_t i = 0; i < kInitialQueueSize; ++i) {
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
}
EXPECT_EQ(frames_queued(), kInitialQueueSize);
constexpr size_t kNumberOfFramesToSubmit = 100;
constexpr int kMinimumNumberOfFramesBetweenDrops = 8;
int processed_frames_since_last_frame_drop = 0;
size_t submitted_frames = kInitialQueueSize;
while (submitted_frames < kNumberOfFramesToSubmit) {
// Every frame will be rendered with occasional frame drops to reduce the
// steady state queue.
// In each while iteration: Enqueue two new frames (60Hz) and render three
// times (90Hz).
for (int i = 0; i < 2; ++i) {
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
if (frames_dropped > 0) {
ASSERT_EQ(frames_dropped, 1u);
EXPECT_GE(processed_frames_since_last_frame_drop,
kMinimumNumberOfFramesBetweenDrops);
processed_frames_since_last_frame_drop = 0;
} else {
++processed_frames_since_last_frame_drop;
}
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
++submitted_frames;
}
size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame);
if (frames_dropped > 0) {
ASSERT_EQ(frames_dropped, 1u);
EXPECT_GE(processed_frames_since_last_frame_drop,
kMinimumNumberOfFramesBetweenDrops);
processed_frames_since_last_frame_drop = 0;
} else {
++processed_frames_since_last_frame_drop;
}
}
// Steady state queue should now have been reduced to one frame + the current
// frame that is also counted.
EXPECT_EQ(frames_queued(), 2u);
}
TEST_F(LowLatencyVideoRendererAlgorithmTest,
RenderFrameImmediatelyAfterOutage) {
constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 600.0 + 1.0e-3); // 600Hz.
constexpr int kMaxCompositionDelayInFrames = 6;
for (int outage_length = 0; outage_length < 100; ++outage_length) {
algorithm_.Reset();
// Process one frame to get the algorithm initialized.
CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(nullptr, kRenderInterval);
ASSERT_TRUE(rendered_frame);
int frame_id_0 = rendered_frame->unique_id();
StepUntilJustBeforeNextFrameIsRendered(kRenderInterval,
rendered_frame->unique_id());
for (int i = 0; i < outage_length; ++i) {
// Try to render, but no new frame has been enqueued so the last frame
// will be rendered again.
rendered_frame = RenderAndStep(nullptr, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), frame_id_0);
}
// Enqueue two frames.
int frame_id_1 = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
int frame_id_2 = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
// The first submitted frame should be rendered.
rendered_frame = RenderAndStep(nullptr, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), frame_id_1);
// The same frame is rendered for 9 more render intervals.
StepUntilJustBeforeNextFrameIsRendered(kRenderInterval, frame_id_1);
// The next frame is rendered.
rendered_frame = RenderAndStep(nullptr, kRenderInterval);
ASSERT_TRUE(rendered_frame);
EXPECT_EQ(rendered_frame->unique_id(), frame_id_2);
}
}
} // namespace blink