blob: e4582b35288e61a532c5b42e83f6577e9b2ccd21 [file]
/*
* Copyright (c) 2026 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 "modules/video_coding/utility/cbr_layer_rate_tracker.h"
#include <array>
#include <optional>
#include <vector>
#include "api/units/data_rate.h"
#include "api/units/time_delta.h"
#include "api/video_codecs/video_encoder_builders.h"
#include "api/video_codecs/video_encoder_interface.h"
#include "test/gtest.h"
namespace webrtc {
namespace {
using FrameType = VideoEncoderInterface::FrameType;
constexpr DataRate kStreamBitrate = DataRate::KilobitsPerSec(600);
// The tracker does not look at the duration.
constexpr TimeDelta kFrameDuration = TimeDelta::Millis(33);
struct Frame {
int spatial_id;
int temporal_id;
DataRate bitrate;
bool is_keyframe;
};
// Feeds the tracker one temporal unit made up of `frames`.
void EncodeTemporalUnit(CbrLayerRateTracker& tracker,
const std::vector<Frame>& frames) {
std::vector<VideoEncoderInterface::FrameEncodeSettings> settings;
for (const Frame& frame : frames) {
settings.push_back(FrameEncodeSettingsBuilder()
.SpatialId(frame.spatial_id)
.TemporalId(frame.temporal_id)
.CbrRateOptions(kFrameDuration, frame.bitrate)
.FrameType(frame.is_keyframe
? FrameType::kKeyframe
: FrameType::kDeltaFrame)
.Build());
}
tracker.OnTemporalUnit(settings);
}
// Feeds the tracker a temporal unit holding a single frame.
void EncodeFrame(CbrLayerRateTracker& tracker,
int spatial_id,
int temporal_id,
DataRate bitrate,
bool is_keyframe) {
EncodeTemporalUnit(tracker,
{{spatial_id, temporal_id, bitrate, is_keyframe}});
}
// The bitrate of temporal layer `tid` in a dyadic L1Tx pattern with
// `num_layers` temporal layers, where the per frame bit budget is halved for
// every step up the temporal layer stack and the stream as a whole targets
// `kStreamBitrate`. Matches
// `TemporalLayerPatternForTest::GeometricDistribution` with a ratio of 0.5.
//
// Every layer above the base one ends up with the same share of the bitrate,
// since its frames are twice as many but half as large as those of the layer
// below it. The base layer holds as many frames as the layer just above it,
// each twice the size, so it gets twice the share.
DataRate GeometricLayerBitrate(int tid, int num_layers) {
return kStreamBitrate * ((tid == 0 ? 2.0 : 1.0) / (num_layers + 1));
}
// Feeds the tracker a keyframe followed by the first delta frame of a dyadic
// L1Tx pattern, which is all the priming needs.
void EncodeKeyframeAndFirstDeltaFrame(CbrLayerRateTracker& tracker,
int num_layers) {
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0,
GeometricLayerBitrate(0, num_layers), /*is_keyframe=*/true);
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/num_layers - 1,
GeometricLayerBitrate(num_layers - 1, num_layers),
/*is_keyframe=*/false);
}
// A dyadic L1T3 pattern. Its first two frames are the ones
// `EncodeKeyframeAndFirstDeltaFrame` feeds, so a run of the pattern picks up
// at frame two.
constexpr std::array<int, 4> kL1T3Pattern = {0, 2, 1, 2};
int L1T3TemporalId(int frame) {
return kL1T3Pattern[frame % kL1T3Pattern.size()];
}
// Feeds `num_frames` frames of the L1T3 pattern, starting at `first_frame`,
// with every layer allocated `scale` times its share of `kStreamBitrate`.
void EncodeL1T3Frames(CbrLayerRateTracker& tracker,
int first_frame,
int num_frames,
double scale = 1.0) {
for (int frame = first_frame; frame < first_frame + num_frames; ++frame) {
const int temporal_id = L1T3TemporalId(frame);
EncodeFrame(tracker, /*spatial_id=*/0, temporal_id,
GeometricLayerBitrate(temporal_id, 3) * scale,
/*is_keyframe=*/false);
}
}
// As `kL1T3Pattern`, for four temporal layers.
constexpr std::array<int, 8> kL1T4Pattern = {0, 3, 2, 3, 1, 3, 2, 3};
int L1T4TemporalId(int frame) {
return kL1T4Pattern[frame % kL1T4Pattern.size()];
}
// The share of `stream_bitrate` that a geometric distribution over
// `num_layers` temporal layers gives to `temporal_id`, and the share it gives
// to all the layers up to and including it.
DataRate LayerShare(DataRate stream_bitrate, int temporal_id, int num_layers) {
return stream_bitrate * ((temporal_id == 0 ? 2.0 : 1.0) / (num_layers + 1));
}
DataRate CumulativeShare(DataRate stream_bitrate,
int temporal_id,
int num_layers) {
return stream_bitrate *
(static_cast<double>(temporal_id + 2) / (num_layers + 1));
}
TEST(CbrLayerRateTrackerTest, SingleLayerReportsTheFullBitrate) {
CbrLayerRateTracker tracker;
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0, kStreamBitrate,
/*is_keyframe=*/true);
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0, kStreamBitrate,
/*is_keyframe=*/false);
EXPECT_EQ(tracker.allocation().num_temporal_layers, 1);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 1);
EXPECT_EQ(tracker.allocation().bitrate[0][0], kStreamBitrate);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate);
}
TEST(CbrLayerRateTrackerTest, NothingIsKnownBeforeTheFirstFrame) {
CbrLayerRateTracker tracker;
EXPECT_EQ(tracker.allocation().num_temporal_layers, 1);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 1);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), DataRate::Zero());
}
TEST(CbrLayerRateTrackerTest, PrimesTwoLayersOnFirstFrameAfterKeyframe) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/2);
EXPECT_EQ(tracker.allocation().num_temporal_layers, 2);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 2);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 1);
// The layers hold half of the frames each, and a base layer frame is twice
// the size of a T1 frame, so the base layer accounts for two thirds of the
// bitrate.
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(), 400, 1);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps(), 1);
}
TEST(CbrLayerRateTrackerTest, PrimesThreeLayersOnFirstFrameAfterKeyframe) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EXPECT_EQ(tracker.allocation().num_temporal_layers, 3);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 4);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 2);
EXPECT_EQ(tracker.allocation().framerate_factor[2], 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(), 300, 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][1].kbps(), 450, 1);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps(), 1);
}
TEST(CbrLayerRateTrackerTest, PrimesFourLayersOnFirstFrameAfterKeyframe) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/4);
EXPECT_EQ(tracker.allocation().num_temporal_layers, 4);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 8);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 4);
EXPECT_EQ(tracker.allocation().framerate_factor[2], 2);
EXPECT_EQ(tracker.allocation().framerate_factor[3], 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(), 240, 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][1].kbps(), 360, 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][2].kbps(), 480, 1);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps(), 1);
}
TEST(CbrLayerRateTrackerTest, SpatialLayersShareTheTemporalStructure) {
CbrLayerRateTracker tracker;
// Two spatial layers, the upper one with twice the bitrate of the lower one,
// in a dyadic L1T2 pattern.
for (int frame = 0; frame < 16; ++frame) {
const int temporal_id = frame % 2;
const DataRate bitrate = GeometricLayerBitrate(temporal_id, 2);
EncodeTemporalUnit(
tracker, {{0, temporal_id, bitrate / 3, /*is_keyframe=*/frame == 0},
{1, temporal_id, bitrate * 2 / 3, /*is_keyframe=*/false}});
}
EXPECT_EQ(tracker.allocation().num_temporal_layers, 2);
EXPECT_EQ(tracker.allocation().framerate_factor[0], 2);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 1);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps() / 3, 5);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(1).kbps(),
2 * kStreamBitrate.kbps() / 3, 5);
}
TEST(CbrLayerRateTrackerTest, ConvergesOnANonGeometricDistribution) {
CbrLayerRateTracker tracker;
// A distribution where the per frame bit budget decreases linearly with the
// temporal id instead of geometrically, so the layers above the base one do
// not end up with equal shares and the priming does not predict it. With the
// frames of an L1T3 pattern distributed 1:1:2 over the layers and per frame
// budgets of 3:2:1, the shares come out as 3:2:2.
const std::array<DataRate, 3> kLayerBitrates = {
kStreamBitrate * 3 / 7,
kStreamBitrate * 2 / 7,
kStreamBitrate * 2 / 7,
};
constexpr std::array<int, 4> kPattern = {0, 2, 1, 2};
EncodeFrame(tracker, 0, 0, kLayerBitrates[0], /*is_keyframe=*/true);
for (int frame = 1; frame < 40; ++frame) {
const int temporal_id = kPattern[frame % 4];
EncodeFrame(tracker, 0, temporal_id, kLayerBitrates[temporal_id],
/*is_keyframe=*/false);
}
EXPECT_EQ(tracker.allocation().framerate_factor[0], 4);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 2);
EXPECT_EQ(tracker.allocation().framerate_factor[2], 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(),
kStreamBitrate.kbps() * 3 / 7, 10);
EXPECT_NEAR(tracker.allocation().bitrate[0][1].kbps(),
kStreamBitrate.kbps() * 5 / 7, 10);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps(), 10);
}
TEST(CbrLayerRateTrackerTest, FollowsAChangeOfTheStreamBitrate) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/2);
// The stream bitrate is halved, keeping the same distribution over the
// temporal layers. Redistributing the same shape over a new bitrate is
// picked up as soon as every layer has been seen once.
for (int frame = 0; frame < 4; ++frame) {
const int temporal_id = frame % 2;
EncodeFrame(tracker, 0, temporal_id,
GeometricLayerBitrate(temporal_id, 2) / 2,
/*is_keyframe=*/false);
}
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps() / 2, 5);
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(), 200, 5);
}
TEST(CbrLayerRateTrackerTest, SingleLayerFollowsTheBitrateImmediately) {
CbrLayerRateTracker tracker;
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0, kStreamBitrate,
/*is_keyframe=*/true);
// With a single temporal layer every frame states the bitrate of the whole
// stream, so there is nothing to average over and no reason to lag behind.
const DataRate kLowerBitrate = kStreamBitrate / 5;
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0, kLowerBitrate,
/*is_keyframe=*/false);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kLowerBitrate);
}
TEST(CbrLayerRateTrackerTest, ConvergesOnAShiftedKeyPattern) {
// In L2T2_KEY_SHIFT the spatial layers run antiphase: within a temporal unit
// they are on different temporal layers, and the base layer of the lower
// spatial layer holds two frames in a row right after the keyframe. The
// frame after the keyframe is therefore not on the topmost layer and the
// priming does not kick in, leaving the cadence to be learned.
//
// t=0: S0T0, S1T0 t=1: S0T0, S1T1 t=2: S0T1, S1T0 ...
CbrLayerRateTracker tracker;
const DataRate kLowerBitrate = kStreamBitrate / 3;
const DataRate kUpperBitrate = kStreamBitrate * 2 / 3;
EncodeTemporalUnit(tracker,
{{0, 0, kLowerBitrate * 2 / 3, /*is_keyframe=*/true},
{1, 0, kUpperBitrate * 2 / 3, /*is_keyframe=*/false}});
for (int temporal_unit = 1; temporal_unit <= 16; ++temporal_unit) {
const int lower_temporal_id = temporal_unit % 2 == 1 ? 0 : 1;
const int upper_temporal_id = 1 - lower_temporal_id;
EncodeTemporalUnit(
tracker, {{0, lower_temporal_id,
kLowerBitrate * (lower_temporal_id == 0 ? 2.0 / 3 : 1.0 / 3),
/*is_keyframe=*/false},
{1, upper_temporal_id,
kUpperBitrate * (upper_temporal_id == 0 ? 2.0 / 3 : 1.0 / 3),
/*is_keyframe=*/false}});
// Both layers have been seen twice after four temporal units, which is all
// it takes to measure how often their frames occur.
if (temporal_unit >= 4) {
EXPECT_EQ(tracker.allocation().framerate_factor[0], 2)
<< " at temporal unit " << temporal_unit;
}
}
EXPECT_EQ(tracker.allocation().num_temporal_layers, 2);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 1);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kLowerBitrate.kbps(), 5);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(1).kbps(),
kUpperBitrate.kbps(), 5);
}
TEST(CbrLayerRateTrackerTest, ConvergesOnANonDyadicCadence) {
// A pattern where only every third frame belongs to the base layer. The
// priming assumes a dyadic pattern, in which the base layer holds every
// other frame, so the cadence has to be corrected from there.
CbrLayerRateTracker tracker;
// The base layer frames are twice the size of the ones above them but only
// half as many, so the two layers end up with the same bitrate.
const DataRate kLayerBitrate = kStreamBitrate / 2;
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0, kLayerBitrate,
/*is_keyframe=*/true);
for (int frame = 1; frame <= 40; ++frame) {
EncodeFrame(tracker, /*spatial_id=*/0,
/*temporal_id=*/frame % 3 == 0 ? 0 : 1, kLayerBitrate,
/*is_keyframe=*/false);
// Measured: the cadence estimate settles on the correct value by the
// twentieth frame, less than a second of video, and stays there. The
// frames of the upper layer come in pairs, so the interval between them
// alternates between one and two and the estimate has to average the two
// out before it can be trusted to the nearest integer.
if (frame >= 20) {
EXPECT_EQ(tracker.allocation().framerate_factor[0], 3)
<< " at frame " << frame;
}
}
EXPECT_EQ(tracker.allocation().num_temporal_layers, 2);
EXPECT_EQ(tracker.allocation().framerate_factor[1], 1);
EXPECT_NEAR(tracker.allocation().bitrate[0][0].kbps(), kLayerBitrate.kbps(),
5);
EXPECT_NEAR(tracker.allocation().SpatialLayerBitrate(0).kbps(),
kStreamBitrate.kbps(), 5);
}
// The layer the caller happens to state a new allocation on first must not
// matter: the change is carried over to the layers that have not reported yet,
// so the cumulative bitrate is right for the very next frame either way. The
// three tests below place the change on each of the layers in turn.
TEST(CbrLayerRateTrackerTest, CarriesAChangeSeenOnTheBaseLayerOver) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
// Run the pattern until every layer has stated a bitrate of its own. The
// frame that follows belongs to the base layer.
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/6);
ASSERT_EQ(L1T3TemporalId(8), 0);
EncodeL1T3Frames(tracker, /*first_frame=*/8, /*num_frames=*/1, /*scale=*/0.5);
EXPECT_EQ(tracker.allocation().bitrate[0][0],
GeometricLayerBitrate(0, 3) / 2);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate / 2);
}
TEST(CbrLayerRateTrackerTest, CarriesAChangeSeenOnAMiddleLayerOver) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/4);
ASSERT_EQ(L1T3TemporalId(6), 1);
EncodeL1T3Frames(tracker, /*first_frame=*/6, /*num_frames=*/1, /*scale=*/0.5);
EXPECT_EQ(tracker.allocation().bitrate[0][0],
GeometricLayerBitrate(0, 3) / 2);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate / 2);
}
TEST(CbrLayerRateTrackerTest, CarriesAChangeSeenOnTheTopLayerOver) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/5);
ASSERT_EQ(L1T3TemporalId(7), 2);
EncodeL1T3Frames(tracker, /*first_frame=*/7, /*num_frames=*/1, /*scale=*/0.5);
EXPECT_EQ(tracker.allocation().bitrate[0][0],
GeometricLayerBitrate(0, 3) / 2);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate / 2);
}
TEST(CbrLayerRateTrackerTest, DoesNotCountACarriedOverChangeTwice) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/4);
// The allocation is halved, and halved again before the layers that were
// only assumed to follow along have stated anything themselves. Each of them
// then restates what was already assumed, which must leave the estimates
// where they are.
EncodeL1T3Frames(tracker, /*first_frame=*/6, /*num_frames=*/1, /*scale=*/0.5);
EncodeL1T3Frames(tracker, /*first_frame=*/7, /*num_frames=*/1,
/*scale=*/0.25);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate / 4);
EncodeL1T3Frames(tracker, /*first_frame=*/8, /*num_frames=*/4,
/*scale=*/0.25);
EXPECT_EQ(tracker.allocation().bitrate[0][0],
GeometricLayerBitrate(0, 3) / 4);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kStreamBitrate / 4);
}
TEST(CbrLayerRateTrackerTest, SettlesOnANewDistribution) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/6);
// The stream bitrate is kept, but distributed 3:2:2 rather than 2:1:1. The
// first layer to state its new share is taken to speak for the others, which
// is wrong here, so a layer that was carried along is only put right the
// next time it states a bitrate of its own.
const std::array<DataRate, 3> kNewBitrates = {
kStreamBitrate * 3 / 7,
kStreamBitrate * 2 / 7,
kStreamBitrate * 2 / 7,
};
// Splitting the stream bitrate in sevenths does not come out even, so the
// layers are what the total is held against.
const DataRate kNewStreamBitrate =
kNewBitrates[0] + kNewBitrates[1] + kNewBitrates[2];
for (int frame = 8; frame < 16; ++frame) {
const int temporal_id = L1T3TemporalId(frame);
EncodeFrame(tracker, 0, temporal_id, kNewBitrates[temporal_id],
/*is_keyframe=*/false);
// The base layer, the last to be heard from a second time, settles on the
// frame that follows a full pattern.
if (frame >= 12) {
EXPECT_EQ(tracker.allocation().bitrate[0][0], kNewBitrates[0])
<< " at frame " << frame;
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kNewStreamBitrate)
<< " at frame " << frame;
}
}
EXPECT_EQ(tracker.allocation().bitrate[0][1],
kNewBitrates[0] + kNewBitrates[1]);
}
TEST(CbrLayerRateTrackerTest, DoesNotReadARepeatedBitrateAsAChange) {
CbrLayerRateTracker tracker;
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
EncodeL1T3Frames(tracker, /*first_frame=*/2, /*num_frames=*/6);
// Only the base layer is given more, which the tracker cannot tell from the
// start of a change of the whole allocation. The layers above it then keep
// restating what they had, and a restatement says nothing, so the estimates
// must settle rather than swing back and forth between the two readings.
const DataRate kNewBaseBitrate = GeometricLayerBitrate(0, 3) * 1.5;
const DataRate kNewStreamBitrate = kNewBaseBitrate +
GeometricLayerBitrate(1, 3) +
GeometricLayerBitrate(2, 3);
for (int frame = 8; frame < 40; ++frame) {
const int temporal_id = L1T3TemporalId(frame);
EncodeFrame(tracker, 0, temporal_id,
temporal_id == 0 ? kNewBaseBitrate
: GeometricLayerBitrate(temporal_id, 3),
/*is_keyframe=*/false);
// One pattern is enough for every layer to have been heard from.
if (frame >= 12) {
EXPECT_EQ(tracker.allocation().bitrate[0][0], kNewBaseBitrate)
<< " at frame " << frame;
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0), kNewStreamBitrate)
<< " at frame " << frame;
}
}
}
TEST(CbrLayerRateTrackerTest, DoesNotReadADepartureFromAGuessAsAChange) {
CbrLayerRateTracker tracker;
// The priming guesses what the layers above the base one hold. The first
// frame of such a layer states the real value, which is not a change of the
// allocation however far off the guess was, so the other layers must be left
// alone.
EncodeKeyframeAndFirstDeltaFrame(tracker, /*num_layers=*/3);
const DataRate kMiddleLayerBitrate = GeometricLayerBitrate(1, 3) * 4;
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/1, kMiddleLayerBitrate,
/*is_keyframe=*/false);
EXPECT_EQ(tracker.allocation().bitrate[0][0], GeometricLayerBitrate(0, 3));
EXPECT_EQ(tracker.allocation().bitrate[0][1],
GeometricLayerBitrate(0, 3) + kMiddleLayerBitrate);
}
TEST(CbrLayerRateTrackerTest, KeepsTheSpatialLayersApart) {
// Two spatial layers in an L1T2 pattern, the lower one at 300 kbps and the
// upper one at 600 kbps, both split 2:1 between their temporal layers.
constexpr DataRate kLowerStreamBitrate = DataRate::KilobitsPerSec(300);
constexpr DataRate kUpperStreamBitrate = DataRate::KilobitsPerSec(600);
CbrLayerRateTracker tracker;
for (int frame = 0; frame < 4; ++frame) {
const double share = frame % 2 == 0 ? 2.0 / 3 : 1.0 / 3;
EncodeTemporalUnit(tracker, {{0, frame % 2, kLowerStreamBitrate * share,
/*is_keyframe=*/frame == 0},
{1, frame % 2, kUpperStreamBitrate * share,
/*is_keyframe=*/false}});
}
// Halving the lower spatial layer says nothing about the upper one, which
// is left out of this temporal unit.
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0,
kLowerStreamBitrate * (2.0 / 3) / 2, /*is_keyframe=*/false);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0),
kLowerStreamBitrate / 2);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(1), kUpperStreamBitrate);
}
// The two simulations below drive the tracker the way a congestion controller
// would drive a stream with temporal layers, which the encoder level tests do
// not cover: those run a single temporal layer, where the bitrate of the layer
// and of the stream are the same thing.
TEST(CbrLayerRateTrackerTest, FollowsAStagedBitrateSweep) {
// The bitrate profile of the encoder level ChangingBitrateTargetVga test,
// over an L1T3 pattern at 30 fps.
constexpr int kNumLayers = 3;
CbrLayerRateTracker tracker;
int frame = 0;
auto encode = [&](DataRate stream_bitrate, int num_frames) {
for (int i = 0; i < num_frames; ++i, ++frame) {
const int temporal_id = L1T3TemporalId(frame);
EncodeFrame(tracker, /*spatial_id=*/0, temporal_id,
LayerShare(stream_bitrate, temporal_id, kNumLayers),
/*is_keyframe=*/frame == 0);
// Every step of the sweep scales the whole allocation, so the frame at
// hand states everything there is to know about the change and the
// bitrate it is encoded against is right away.
EXPECT_EQ(tracker.allocation().bitrate[0][temporal_id],
CumulativeShare(stream_bitrate, temporal_id, kNumLayers))
<< " at frame " << frame << " of T" << temporal_id;
}
};
encode(DataRate::KilobitsPerSec(500), 60);
encode(DataRate::KilobitsPerSec(100), 30);
for (int kbps = 150; kbps < 500; kbps += 50) {
encode(DataRate::KilobitsPerSec(kbps), 6);
}
encode(DataRate::KilobitsPerSec(500), 30);
EXPECT_EQ(tracker.allocation().SpatialLayerBitrate(0),
DataRate::KilobitsPerSec(500));
}
TEST(CbrLayerRateTrackerTest, RecoversOnceTheTargetHoldsStill) {
// A target that moves on every single frame, which is more than a congestion
// controller would ask for, denies the tracker the one thing that tells a
// change of the whole allocation from a change of the distribution: a layer
// restating the bitrate it already had. Whatever the stream does while a
// layer waits for its first turn is then never accounted for, and the skew
// that leaves behind, measured at up to 10% of the cumulative bitrate,
// stays until the target settles.
constexpr int kNumLayers = 4;
CbrLayerRateTracker tracker;
auto stream_bitrate_at = [](int frame) {
// A sawtooth between 300 and 900 kbps in steps of 30, which divides evenly
// into fifths and keeps the layer bitrates whole.
return DataRate::KilobitsPerSec(300 + 30 * (frame % 21));
};
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/0,
LayerShare(stream_bitrate_at(0), 0, kNumLayers),
/*is_keyframe=*/true);
EncodeFrame(tracker, /*spatial_id=*/0, /*temporal_id=*/kNumLayers - 1,
LayerShare(stream_bitrate_at(1), kNumLayers - 1, kNumLayers),
/*is_keyframe=*/false);
for (int frame = 2; frame < 40; ++frame) {
const int temporal_id = L1T4TemporalId(frame);
EncodeFrame(tracker,
/*spatial_id=*/0, temporal_id,
LayerShare(stream_bitrate_at(frame), temporal_id, kNumLayers),
/*is_keyframe=*/false);
}
// A layer restating what it holds is what puts the estimates right, so they
// are all in order again once the pattern has come around.
constexpr DataRate kHeldBitrate = DataRate::KilobitsPerSec(600);
for (int frame = 40; frame < 80; ++frame) {
const int temporal_id = L1T4TemporalId(frame);
EncodeFrame(tracker, /*spatial_id=*/0, temporal_id,
LayerShare(kHeldBitrate, temporal_id, kNumLayers),
/*is_keyframe=*/false);
if (frame >= 40 + 1 * static_cast<int>(kL1T4Pattern.size())) {
EXPECT_EQ(tracker.allocation().bitrate[0][temporal_id],
CumulativeShare(kHeldBitrate, temporal_id, kNumLayers))
<< " at frame " << frame << " of T" << temporal_id;
}
}
}
TEST(CbrLayerRateTrackerTest, PrimesAllSpatialLayersOnFirstUnitAfterKeyframe) {
// L2T3: the temporal unit after the keyframe is on the topmost temporal
// layer in both spatial layers, so both are primed.
CbrLayerRateTracker tracker;
EncodeTemporalUnit(tracker, {{0, 0, GeometricLayerBitrate(0, 3) / 3,
/*is_keyframe=*/true},
{1, 0, GeometricLayerBitrate(0, 3) * 2 / 3,
/*is_keyframe=*/false}});
EncodeTemporalUnit(tracker, {{0, 2, GeometricLayerBitrate(2, 3) / 3,
/*is_keyframe=*/false},
{1, 2, GeometricLayerBitrate(2, 3) * 2 / 3,
/*is_keyframe=*/false}});
const CumulativeCbrAllocation& allocation = tracker.allocation();
EXPECT_EQ(allocation.num_temporal_layers, 3);
EXPECT_EQ(allocation.framerate_factor[0], 4);
EXPECT_EQ(allocation.framerate_factor[1], 2);
EXPECT_EQ(allocation.framerate_factor[2], 1);
EXPECT_NEAR(allocation.bitrate[0][0].kbps(), 100, 1);
EXPECT_NEAR(allocation.bitrate[0][1].kbps(), 150, 1);
EXPECT_NEAR(allocation.SpatialLayerBitrate(0).kbps(), 200, 1);
EXPECT_NEAR(allocation.bitrate[1][0].kbps(), 200, 1);
EXPECT_NEAR(allocation.bitrate[1][1].kbps(), 300, 1);
EXPECT_NEAR(allocation.SpatialLayerBitrate(1).kbps(), 400, 1);
EXPECT_NEAR(allocation.TotalBitrate().kbps(), kStreamBitrate.kbps(), 2);
}
TEST(CbrLayerRateTrackerTest, AbsentSpatialLayersKeepTheirBitrate) {
// Three spatial layers where a spatial layer being present means that all
// layers above it are present too, and the lowest present one follows the
// dyadic cadence {0, 2, 1, 2}. All frames are on the base temporal layer, so
// the spatial layers simply run at different frame rates.
constexpr std::array<int, 4> kLowestLayer = {0, 2, 1, 2};
const std::array<DataRate, 3> kLayerBitrates = {
DataRate::KilobitsPerSec(50), DataRate::KilobitsPerSec(100),
DataRate::KilobitsPerSec(150)};
CbrLayerRateTracker tracker;
std::optional<CumulativeCbrAllocation> previous;
for (int temporal_unit = 0; temporal_unit < 16; ++temporal_unit) {
std::vector<Frame> frames;
for (int sid = kLowestLayer[temporal_unit % 4]; sid < 3; ++sid) {
frames.push_back({sid, 0, kLayerBitrates[sid],
/*is_keyframe=*/temporal_unit == 0 && sid == 0});
}
EncodeTemporalUnit(tracker, frames);
// Once every layer has been heard from, the allocation must not move
// whichever layers the temporal unit holds, or encoders that reconfigure
// on every change would do so for nothing.
if (temporal_unit >= 1) {
ASSERT_TRUE(previous.has_value());
EXPECT_EQ(tracker.allocation(), *previous)
<< " at temporal unit " << temporal_unit;
}
previous = tracker.allocation();
}
const CumulativeCbrAllocation& allocation = tracker.allocation();
EXPECT_EQ(allocation.num_temporal_layers, 1);
for (int sid = 0; sid < 3; ++sid) {
EXPECT_EQ(allocation.SpatialLayerBitrate(sid), kLayerBitrates[sid]);
for (int tid = 0; tid < CumulativeCbrAllocation::kMaxTemporalLayers;
++tid) {
EXPECT_EQ(allocation.bitrate[sid][tid], kLayerBitrates[sid]);
}
}
EXPECT_EQ(allocation.TotalBitrate(), DataRate::KilobitsPerSec(300));
}
TEST(CbrLayerRateTrackerTest, IgnoresFramesWithoutCbr) {
CbrLayerRateTracker tracker;
std::vector<VideoEncoderInterface::FrameEncodeSettings> settings;
settings.push_back(FrameEncodeSettingsBuilder()
.SpatialId(0)
.TemporalId(0)
.CqpRateOptions(/*target_qp=*/30)
.FrameType(FrameType::kKeyframe)
.Build());
tracker.OnTemporalUnit(settings);
EXPECT_EQ(tracker.allocation(), CumulativeCbrAllocation());
}
} // namespace
} // namespace webrtc