Commit 68154c1d authored by Johannes Kron's avatar Johannes Kron Committed by Commit Bot

Add support for generic display rates to WebRTC/low-latency renderer

The initial implementation of the low-latency renderer assumed
that the display rate was fixed at 60Hz. This CL adds support
for generic frame rates as well as missed render intervals.

The low-latency renderer algorithm is enabled from WebRTC by
setting the metadata field maximum_composition_delay_in_frames.

This is an experimental feature that is active if and only if the
RTP header extension playout-delay is set to min=0ms and max>0ms.

The feature can be completely disabled by specifying the field trial
WebRTC-LowLatencyRenderer/enabled:false/

Bug: 1138888
Change-Id: Ic64b26331d31aaf8fb8ed4fb8f1056b227cb6eaa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2527424Reviewed-by: default avatarGuido Urdaneta <guidou@chromium.org>
Reviewed-by: default avatarJesse Doherty <jwd@chromium.org>
Reviewed-by: default avatarDale Curtis <dalecurtis@chromium.org>
Commit-Queue: Johannes Kron <kron@chromium.org>
Cr-Commit-Position: refs/heads/master@{#830225}
parent 4d61874e
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#include "third_party/blink/renderer/modules/mediastream/low_latency_video_renderer_algorithm.h" #include "third_party/blink/renderer/modules/mediastream/low_latency_video_renderer_algorithm.h"
#include <algorithm>
#include "base/metrics/histogram_functions.h"
#include "media/base/media_log.h" #include "media/base/media_log.h"
namespace blink { namespace blink {
...@@ -20,98 +23,224 @@ scoped_refptr<media::VideoFrame> LowLatencyVideoRendererAlgorithm::Render( ...@@ -20,98 +23,224 @@ scoped_refptr<media::VideoFrame> LowLatencyVideoRendererAlgorithm::Render(
base::TimeTicks deadline_max, base::TimeTicks deadline_max,
size_t* frames_dropped) { size_t* frames_dropped) {
DCHECK_LE(deadline_min, deadline_max); DCHECK_LE(deadline_min, deadline_max);
// TODO(crbug.com/1138888): Handle the case where the screen refresh rate and
// the video frame rate are not the same as well as occasional skips of
// rendering intervals.
if (frames_dropped) { if (frames_dropped) {
*frames_dropped = 0; *frames_dropped = 0;
} }
if (frame_queue_.size() > 1) { stats_.accumulated_queue_length += frame_queue_.size();
constexpr size_t kMaxQueueSize = 30; ++stats_.accumulated_queue_length_count;
if (frame_queue_.size() > kMaxQueueSize) {
// The queue has grown too big. Clear all but the last enqueued frame and // Determine how many fractional frames that should be rendered based on how
// enter normal mode. // much time has passed since the last renderer deadline.
if (frames_dropped) { double fractional_frames_to_render = 1.0;
*frames_dropped += frame_queue_.size() - 1; if (last_render_deadline_min_) {
base::TimeDelta elapsed_time = deadline_min - *last_render_deadline_min_;
fractional_frames_to_render =
elapsed_time.InMillisecondsF() /
average_frame_duration().InMillisecondsF() +
unrendered_fractional_frames_;
} }
while (frame_queue_.size() > 1) { size_t number_of_frames_to_render =
DetermineModeAndNumberOfFramesToRender(fractional_frames_to_render);
if (mode_ == Mode::kDrain) {
// Render twice as many frames in drain mode.
fractional_frames_to_render *= 2.0;
stats_.drained_frames +=
(fractional_frames_to_render - number_of_frames_to_render);
number_of_frames_to_render = fractional_frames_to_render;
} else if (ReduceSteadyStateQueue(number_of_frames_to_render)) {
// Increment counters to drop one extra frame.
++fractional_frames_to_render;
++number_of_frames_to_render;
++stats_.reduce_steady_state;
}
// Limit |number_of_frames_to_render| to a valid number. +1 in the min
// operation to make sure that number_of_frames_to_render is not set to zero
// unless it already was zero. |number_of_frames_to_render| > 0 signals that
// enough time has passed so that a new frame should be rendered if possible.
number_of_frames_to_render =
std::min<size_t>(number_of_frames_to_render, frame_queue_.size() + 1);
// Pop frames that should be dropped.
for (size_t i = 1; i < number_of_frames_to_render; ++i) {
frame_queue_.pop_front(); frame_queue_.pop_front();
if (frames_dropped) {
++(*frames_dropped);
}
}
if (number_of_frames_to_render > 0) {
SelectNextAvailableFrameAndUpdateLastDeadline(deadline_min);
unrendered_fractional_frames_ =
fractional_frames_to_render - number_of_frames_to_render;
stats_.dropped_frames += number_of_frames_to_render - 1;
++stats_.render_frame;
}
if (last_deadline_min_stats_recorded_) {
// Record stats for every 100 s, corresponding to roughly 6000 frames in
// normal conditions.
if (deadline_min - *last_deadline_min_stats_recorded_ >
base::TimeDelta::FromSeconds(100)) {
RecordAndResetStats();
last_deadline_min_stats_recorded_ = deadline_min;
}
} else {
last_deadline_min_stats_recorded_ = deadline_min;
} }
return current_frame_;
}
void LowLatencyVideoRendererAlgorithm::Reset() {
last_render_deadline_min_.reset();
current_frame_.reset();
frame_queue_.clear();
mode_ = Mode::kNormal; mode_ = Mode::kNormal;
unrendered_fractional_frames_ = 0;
consecutive_frames_with_back_up_ = 0;
stats_ = {};
}
void LowLatencyVideoRendererAlgorithm::EnqueueFrame(
scoped_refptr<media::VideoFrame> frame) {
DCHECK(frame);
DCHECK(!frame->metadata()->end_of_stream);
frame_queue_.push_back(std::move(frame));
++stats_.total_frames;
}
size_t LowLatencyVideoRendererAlgorithm::DetermineModeAndNumberOfFramesToRender(
double fractional_frames_to_render) {
// Determine number of entire frames that should be rendered and update
// mode_.
size_t number_of_frames_to_render = fractional_frames_to_render;
if (number_of_frames_to_render < frame_queue_.size()) {
// |kMaxQueueSize| is a safety mechanism that should be activated only in
// rare circumstances. The drain mode should normally take care of high
// queue levels. |kMaxQueueSize| should be set to the lowest possible value
// that doesn't shortcut the drain mode. If the number of frames in the
// queue is too high, we may run out of buffers in the HW decoder resulting
// in a fallback to SW decoder.
constexpr size_t kMaxQueueSize = 7;
if (frame_queue_.size() > kMaxQueueSize) {
// Clear all but the last enqueued frame and enter normal mode.
number_of_frames_to_render = frame_queue_.size();
mode_ = Mode::kNormal;
++stats_.max_size_drop_queue;
} else { } else {
// There are several frames in the queue, determine if we should enter // There are several frames in the queue, determine if we should enter
// drain mode based on queue length and the maximum composition delay that // drain mode based on queue length and the maximum composition delay that
// is provided for the last enqueued frame. // is provided for the last enqueued frame.
constexpr size_t kDefaultMaxCompositionDelayInFrames = 10; constexpr size_t kDefaultMaxCompositionDelayInFrames = 6;
int max_queue_length = frame_queue_.back() int max_remaining_queue_length =
frame_queue_.back()
->metadata() ->metadata()
->maximum_composition_delay_in_frames.value_or( ->maximum_composition_delay_in_frames.value_or(
kDefaultMaxCompositionDelayInFrames); kDefaultMaxCompositionDelayInFrames);
// The number of frames in the queue is in the range [2, kMaxQueueSize] // The number of frames in the queue is in the range
// due to the conditions that lead up to this point. This means that the // [number_of_frames_to_render + 1, kMaxQueueSize] due to the conditions
// active range of |max_queue_length| is [1, kMaxQueueSize]. // that lead up to this point. This means that the active range of
if (max_queue_length < static_cast<int>(frame_queue_.size())) // |max_queue_length| is [1, kMaxQueueSize].
if (max_remaining_queue_length <
static_cast<int>(frame_queue_.size() - number_of_frames_to_render +
1)) {
mode_ = Mode::kDrain; mode_ = Mode::kDrain;
++stats_.enter_drain_mode;
if (mode_ == Mode::kDrain) {
// Drop one frame if we're in drain moide.
frame_queue_.pop_front();
if (frames_dropped) {
++(*frames_dropped);
}
} }
} }
} else if (mode_ == Mode::kDrain) { } else if (mode_ == Mode::kDrain) {
// At most one frame in the queue, exit drain mode. // At most one frame in the queue, exit drain mode.
mode_ = Mode::kNormal; mode_ = Mode::kNormal;
} }
return number_of_frames_to_render;
}
// Reduce steady-state queue length. bool LowLatencyVideoRendererAlgorithm::ReduceSteadyStateQueue(
// Drop one frame if we have observed 10 consecutive rendered frames where size_t number_of_frames_to_render) {
// there was a newer frame in the queue that could have been selected. // Reduce steady state queue if we have observed 10 consecutive rendered
// frames where there was a newer frame in the queue that could have been
// selected.
bool reduce_steady_state_queue = false;
constexpr int kReduceSteadyStateQueueSizeThreshold = 10; constexpr int kReduceSteadyStateQueueSizeThreshold = 10;
if (mode_ == Mode::kNormal && frame_queue_.size() >= 2) { // Has enough time passed so that at least one frame should be rendered?
if (number_of_frames_to_render > 0) {
// Is there a newer frame in the queue that could have been rendered?
if (frame_queue_.size() >= number_of_frames_to_render + 1) {
if (++consecutive_frames_with_back_up_ > if (++consecutive_frames_with_back_up_ >
kReduceSteadyStateQueueSizeThreshold) { kReduceSteadyStateQueueSizeThreshold) {
frame_queue_.pop_front(); reduce_steady_state_queue = true;
if (frames_dropped) {
++(*frames_dropped);
}
consecutive_frames_with_back_up_ = 0; consecutive_frames_with_back_up_ = 0;
} }
} else { } else {
consecutive_frames_with_back_up_ = 0; consecutive_frames_with_back_up_ = 0;
} }
}
return reduce_steady_state_queue;
}
void LowLatencyVideoRendererAlgorithm::
SelectNextAvailableFrameAndUpdateLastDeadline(
base::TimeTicks deadline_min) {
if (frame_queue_.empty()) {
// No frame to render, reset |last_render_deadline_min_| so that the next
// available frame is rendered immediately.
last_render_deadline_min_.reset();
++stats_.no_new_frame_to_render;
} else {
// Select the first frame in the queue to be rendered. // Select the first frame in the queue to be rendered.
if (!frame_queue_.empty()) {
current_frame_.swap(frame_queue_.front()); current_frame_.swap(frame_queue_.front());
frame_queue_.pop_front(); frame_queue_.pop_front();
last_render_deadline_min_ = deadline_min;
} }
// Update the current render interval for subroutines.
render_interval_ = deadline_max - deadline_min;
return current_frame_;
} }
void LowLatencyVideoRendererAlgorithm::Reset() { void LowLatencyVideoRendererAlgorithm::RecordAndResetStats() {
render_interval_ = base::TimeDelta(); // Record UMA stats for sanity check and tuning of the algorithm if needed.
current_frame_.reset(); std::string uma_prefix = "Media.RtcLowLatencyVideoRenderer";
frame_queue_.clear(); // Total frames count.
mode_ = Mode::kNormal; base::UmaHistogramCounts10000(uma_prefix + ".TotalFrames",
consecutive_frames_with_back_up_ = 0; stats_.total_frames);
} if (stats_.total_frames > 0) {
// Dropped frames per mille (=percentage scaled by 10 to get an integer
// between 0-1000).
base::UmaHistogramCounts1000(
uma_prefix + ".DroppedFramesPermille",
1000 * stats_.dropped_frames / stats_.total_frames);
// Drained frames per mille.
base::UmaHistogramCounts1000(
uma_prefix + ".DrainedFramesPermille",
1000 * stats_.drained_frames / stats_.total_frames);
}
void LowLatencyVideoRendererAlgorithm::EnqueueFrame( // Render frame count.
scoped_refptr<media::VideoFrame> frame) { base::UmaHistogramCounts10000(uma_prefix + ".TryToRenderFrameCount",
DCHECK(frame); stats_.render_frame);
DCHECK(!frame->metadata()->end_of_stream); if (stats_.render_frame > 0) {
frame_queue_.push_back(std::move(frame)); // No new frame to render per mille.
base::UmaHistogramCounts1000(
uma_prefix + ".NoNewFrameToRenderPermille",
1000 * stats_.no_new_frame_to_render / stats_.render_frame);
}
// Average queue length x 10 since this is expected to be in the range 1-3
// frames.
CHECK_GT(stats_.accumulated_queue_length_count, 0);
base::UmaHistogramCounts1000(uma_prefix + ".AverageQueueLengthX10",
10 * stats_.accumulated_queue_length /
stats_.accumulated_queue_length_count);
// Enter drain mode count.
base::UmaHistogramCounts10000(uma_prefix + ".EnterDrainModeCount",
stats_.enter_drain_mode);
// Reduce steady state count.
base::UmaHistogramCounts1000(uma_prefix + ".ReduceSteadyStateCount",
stats_.reduce_steady_state);
// Max size drop queue count.
base::UmaHistogramCounts1000(uma_prefix + ".MaxSizeDropQueueCount",
stats_.max_size_drop_queue);
// Clear all stats.
stats_ = {};
} }
} // namespace blink } // namespace blink
...@@ -64,15 +64,27 @@ class MODULES_EXPORT LowLatencyVideoRendererAlgorithm { ...@@ -64,15 +64,27 @@ class MODULES_EXPORT LowLatencyVideoRendererAlgorithm {
} }
private: private:
size_t DetermineModeAndNumberOfFramesToRender(
double fractional_frames_to_render);
bool ReduceSteadyStateQueue(size_t number_of_frames_to_render);
void SelectNextAvailableFrameAndUpdateLastDeadline(
base::TimeTicks deadline_min);
scoped_refptr<media::VideoFrame> current_frame_; scoped_refptr<media::VideoFrame> current_frame_;
// Queue of incoming frames waiting for rendering. // Queue of incoming frames waiting for rendering.
using VideoFrameQueue = WTF::Deque<scoped_refptr<media::VideoFrame>>; using VideoFrameQueue = WTF::Deque<scoped_refptr<media::VideoFrame>>;
VideoFrameQueue frame_queue_; VideoFrameQueue frame_queue_;
// The length of the last deadline interval given to Render(), updated at the // Render deadline min for when the last frame was rendered.
// start of Render(). base::Optional<base::TimeTicks> last_render_deadline_min_;
base::TimeDelta render_interval_;
// Stores the number of fractional frames that were not rendered as of
// |last_render_deadline_min_|. This is needed in case the display refresh
// rate is not a multiple of the video stream frame rate.
double unrendered_fractional_frames_;
enum class Mode { enum class Mode {
// Render frames at their intended rate. // Render frames at their intended rate.
...@@ -87,6 +99,22 @@ class MODULES_EXPORT LowLatencyVideoRendererAlgorithm { ...@@ -87,6 +99,22 @@ class MODULES_EXPORT LowLatencyVideoRendererAlgorithm {
// The number of consecutive render frames with a post-decode queue back-up // The number of consecutive render frames with a post-decode queue back-up
// (defined as greater than one frame). // (defined as greater than one frame).
int consecutive_frames_with_back_up_; int consecutive_frames_with_back_up_;
struct Stats {
int total_frames;
int dropped_frames;
int drained_frames;
int render_frame;
int no_new_frame_to_render;
int accumulated_queue_length;
int accumulated_queue_length_count;
int enter_drain_mode;
int reduce_steady_state;
int max_size_drop_queue;
};
Stats stats_;
base::Optional<base::TimeTicks> last_deadline_min_stats_recorded_;
void RecordAndResetStats();
}; };
} // namespace blink } // namespace blink
......
...@@ -30,17 +30,55 @@ class LowLatencyVideoRendererAlgorithmTest : public testing::Test { ...@@ -30,17 +30,55 @@ class LowLatencyVideoRendererAlgorithmTest : public testing::Test {
return frame; 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(); } size_t frames_queued() const { return algorithm_.frames_queued(); }
scoped_refptr<media::VideoFrame> RenderAndStep(size_t* frames_dropped) { scoped_refptr<media::VideoFrame> RenderAndStep(size_t* frames_dropped) {
constexpr base::TimeDelta kRenderInterval = constexpr base::TimeDelta kRenderInterval =
base::TimeDelta::FromMillisecondsD(1000.0 / 60.0); // 60fps. 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_; const base::TimeTicks start = current_render_time_;
current_render_time_ += kRenderInterval; current_render_time_ += render_interval;
const base::TimeTicks end = current_render_time_; const base::TimeTicks end = current_render_time_;
return algorithm_.Render(start, end, frames_dropped); 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: protected:
media::VideoFramePool frame_pool_; media::VideoFramePool frame_pool_;
LowLatencyVideoRendererAlgorithm algorithm_; LowLatencyVideoRendererAlgorithm algorithm_;
...@@ -58,15 +96,12 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, Empty) { ...@@ -58,15 +96,12 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, Empty) {
EXPECT_EQ(0u, frames_queued()); EXPECT_EQ(0u, frames_queued());
} }
TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode) { TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode60Hz) {
// Every frame rendered. // Every frame rendered.
constexpr int kNumberOfFrames = 100; constexpr int kNumberOfFrames = 100;
constexpr int kMaxCompositionDelayInFrames = 6; constexpr int kMaxCompositionDelayInFrames = 6;
for (int i = 0; i < kNumberOfFrames; ++i) { for (int i = 0; i < kNumberOfFrames; ++i) {
scoped_refptr<media::VideoFrame> frame = int frame_id = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
CreateFrame(kMaxCompositionDelayInFrames);
int frame_id = frame->unique_id();
algorithm_.EnqueueFrame(std::move(frame));
size_t frames_dropped = 0u; size_t frames_dropped = 0u;
scoped_refptr<media::VideoFrame> rendered_frame = scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped); RenderAndStep(&frames_dropped);
...@@ -76,16 +111,150 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode) { ...@@ -76,16 +111,150 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, NormalMode) {
} }
} }
TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode) { // 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. // Enter drain mode when more than 6 frames are in the queue.
constexpr int kMaxCompositionDelayInFrames = 6; constexpr int kMaxCompositionDelayInFrames = 6;
constexpr int kNumberOfFramesSubmitted = kMaxCompositionDelayInFrames + 1; constexpr int kNumberOfFramesSubmitted = kMaxCompositionDelayInFrames + 1;
std::queue<int> enqueued_frame_ids; std::queue<int> enqueued_frame_ids;
for (int i = 0; i < kNumberOfFramesSubmitted; ++i) { for (int i = 0; i < kNumberOfFramesSubmitted; ++i) {
scoped_refptr<media::VideoFrame> frame = enqueued_frame_ids.push(
CreateFrame(kMaxCompositionDelayInFrames); CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
} }
// Every other frame will be rendered until there's one frame in the queue. // Every other frame will be rendered until there's one frame in the queue.
int processed_frames_count = 0; int processed_frames_count = 0;
...@@ -112,16 +281,14 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode) { ...@@ -112,16 +281,14 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode) {
EXPECT_EQ(enqueued_frame_ids.size(), 0u); EXPECT_EQ(enqueued_frame_ids.size(), 0u);
} }
TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode) { TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode60Hz) {
// Enter drain mode when more than 6 frames are in the queue. // Enter drain mode when more than 6 frames are in the queue.
constexpr int kMaxCompositionDelayInFrames = 6; constexpr int kMaxCompositionDelayInFrames = 6;
int number_of_frames_submitted = kMaxCompositionDelayInFrames + 1; int number_of_frames_submitted = kMaxCompositionDelayInFrames + 1;
std::queue<int> enqueued_frame_ids; std::queue<int> enqueued_frame_ids;
for (int i = 0; i < number_of_frames_submitted; ++i) { for (int i = 0; i < number_of_frames_submitted; ++i) {
scoped_refptr<media::VideoFrame> frame = enqueued_frame_ids.push(
CreateFrame(kMaxCompositionDelayInFrames); CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
} }
// Every other frame will be rendered until there's one frame in the queue. // Every other frame will be rendered until there's one frame in the queue.
...@@ -136,10 +303,8 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode) { ...@@ -136,10 +303,8 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode) {
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front()); EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop(); enqueued_frame_ids.pop();
// Enqueue a new frame. // Enqueue a new frame.
scoped_refptr<media::VideoFrame> frame = enqueued_frame_ids.push(
CreateFrame(kMaxCompositionDelayInFrames); CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
++number_of_frames_submitted; ++number_of_frames_submitted;
processed_frames_count += 1 + frames_dropped; processed_frames_count += 1 + frames_dropped;
} }
...@@ -154,23 +319,61 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode) { ...@@ -154,23 +319,61 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, ExitDrainMode) {
EXPECT_EQ(frames_dropped, 0u); EXPECT_EQ(frames_dropped, 0u);
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front()); EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop(); enqueued_frame_ids.pop();
scoped_refptr<media::VideoFrame> frame = enqueued_frame_ids.push(
CreateFrame(kMaxCompositionDelayInFrames); CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
} }
} }
TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction) { // Double Rate Drain (120Hz playing back 60Hz video in DRAIN mode)
// Create an initial queue of 8 frames. TEST_F(LowLatencyVideoRendererAlgorithmTest, EnterDrainMode120Hz) {
constexpr int kMaxCompositionDelayInFrames = 10; constexpr base::TimeDelta kRenderInterval =
constexpr size_t kInitialQueueSize = 8; 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; std::queue<int> enqueued_frame_ids;
for (size_t i = 0; i < kInitialQueueSize; ++i) { for (size_t i = 0; i < kInitialQueueSize; ++i) {
scoped_refptr<media::VideoFrame> frame = enqueued_frame_ids.push(
CreateFrame(kMaxCompositionDelayInFrames); CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
} }
EXPECT_EQ(frames_queued(), kInitialQueueSize); EXPECT_EQ(frames_queued(), kInitialQueueSize);
...@@ -197,11 +400,8 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction) { ...@@ -197,11 +400,8 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction) {
EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front()); EXPECT_EQ(rendered_frame->unique_id(), enqueued_frame_ids.front());
enqueued_frame_ids.pop(); enqueued_frame_ids.pop();
// Enqueue a new frame. enqueued_frame_ids.push(
scoped_refptr<media::VideoFrame> frame = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames));
CreateFrame(kMaxCompositionDelayInFrames);
enqueued_frame_ids.push(frame->unique_id());
algorithm_.EnqueueFrame(std::move(frame));
} }
// Steady state queue should now have been reduced to one frame + the current // Steady state queue should now have been reduced to one frame + the current
...@@ -209,27 +409,106 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction) { ...@@ -209,27 +409,106 @@ TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateQueueReduction) {
EXPECT_EQ(frames_queued(), 2u); EXPECT_EQ(frames_queued(), 2u);
} }
TEST_F(LowLatencyVideoRendererAlgorithmTest, // Fractional rate, steady state queue reduction.
DropAllFramesIfQueueExceedsMaxSize) { TEST_F(LowLatencyVideoRendererAlgorithmTest, SteadyStateReduction90Hz) {
// Create an initial queue of 60 frames. constexpr base::TimeDelta kRenderInterval =
constexpr int kMaxCompositionDelayInFrames = 10; base::TimeDelta::FromMillisecondsD(1000.0 / 90.0); // 90Hz.
constexpr size_t kInitialQueueSize = 60;
int last_id = 0; // Create an initial queue of 5 frames.
constexpr int kMaxCompositionDelayInFrames = 6;
constexpr size_t kInitialQueueSize = 5;
for (size_t i = 0; i < kInitialQueueSize; ++i) { for (size_t i = 0; i < kInitialQueueSize; ++i) {
scoped_refptr<media::VideoFrame> frame = CreateAndEnqueueFrame(kMaxCompositionDelayInFrames);
CreateFrame(kMaxCompositionDelayInFrames);
last_id = frame->unique_id();
algorithm_.EnqueueFrame(std::move(frame));
} }
EXPECT_EQ(frames_queued(), kInitialQueueSize); EXPECT_EQ(frames_queued(), kInitialQueueSize);
// Last submitted frame should be rendered. 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; size_t frames_dropped = 0;
scoped_refptr<media::VideoFrame> rendered_frame = scoped_refptr<media::VideoFrame> rendered_frame =
RenderAndStep(&frames_dropped); RenderAndStep(&frames_dropped, kRenderInterval);
ASSERT_TRUE(rendered_frame); ASSERT_TRUE(rendered_frame);
EXPECT_EQ(frames_dropped, kInitialQueueSize - 1); if (frames_dropped > 0) {
EXPECT_EQ(rendered_frame->unique_id(), last_id); 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 } // namespace blink
...@@ -3042,6 +3042,103 @@ reviews. Googlers can read more about this at go/gwsq-gerrit. ...@@ -3042,6 +3042,103 @@ reviews. Googlers can read more about this at go/gwsq-gerrit.
<summary>Video width while remoting content.</summary> <summary>Video width while remoting content.</summary>
</histogram> </histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.AverageQueueLengthX10"
units="frames" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
10 times the average queue length when Render() is called in the RTC
low-latency video renderer. Repeatedly measured with a period of 100 s for
as long as the stream is active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.DrainedFramesPermille"
units="permille" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Per mille of frames drained in the RTC low-latency video renderer.
Repeatedly measured with a period of 100 s for as long as the stream is
active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.DroppedFramesPermille"
units="permille" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Per mille of frames dropped in the RTC low-latency video renderer.
Repeatedly measured with a period of 100 s for as long as the stream is
active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.EnterDrainModeCount"
units="count" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Number of times drain mode is entered. Repeatedly measured with a period of
100 s for as long as the stream is active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.MaxSizeDropQueueCount"
units="count" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Number of times the maximum queue size is exceeded and the entire queue is
dropped. Repeatedly measured with a period of 100 s for as long as the
stream is active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.NoNewFrameToRenderPermille"
units="permille" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Per mille of render times where the queue is empty and there is no new frame
to render. Repeatedly measured with a period of 100 s for as long as the
stream is active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.ReduceSteadyStateCount"
units="count" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Number of times the reduce steady state queue feature is activated.
Repeatedly measured with a period of 100 s for as long as the stream is
active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.TotalFrames" units="frames"
expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Total number of frames enqueued to the RTC low-latency video renderer.
Repeatedly measured with a period of 100 s for as long as the stream is
active.
</summary>
</histogram>
<histogram name="Media.RtcLowLatencyVideoRenderer.TryToRenderFrameCount"
units="count" expires_after="2021-05-31">
<owner>kron@chromium.org</owner>
<owner>webrtc-video@google.com</owner>
<summary>
Number of times we try to render a new frame. Repeatedly measured with a
period of 100 s for as long as the stream is active.
</summary>
</histogram>
<histogram name="Media.RTCVideoDecoderError" enum="VideoDecodeAcceleratorError" <histogram name="Media.RTCVideoDecoderError" enum="VideoDecodeAcceleratorError"
expires_after="M82"> expires_after="M82">
<owner>posciak@chromium.org</owner> <owner>posciak@chromium.org</owner>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment