| /* |
| * 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 |