feat(quality): the creator sets the rate — per tier, persisted, clamped only at YouTube's floor
The tier table cannot know a creator's connection, so it should not pretend to. The Mbps box under the picker is theirs: the tier chooses geometry and frame rate, the box chooses bits. Stored per tier (`Video.Rate.<tier>`), so tuning 1080p60 does not touch 720p30, and none means "the shipped default" rather than zero. Clamped at the tier's YouTube minimum (`RateLadder`), because a target under it cannot hold the resolution; no ceiling otherwise, because a wrong guess is exactly what live step-down exists to catch, and a creator who types 25 on a fibre line should be allowed to find out. The tooltip carries YouTube's number for the tier. `MainViewModel.EffectiveQuality` is the single substitution point, and the encoder options, the step-down policy and the clip buffer all read it — so a rated tier can never stream at the shipped default because one call site was missed. One ordering trap found by the test: the constructor resolves the default tier before the layout store is open, so a read at that point must not cache, or the shipped default shadows the creator's stored rate for the whole run. 670 pass, 0 warnings.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The creator's own video rate: per tier, clamped at the tier's YouTube minimum, persisted across
|
||||
/// runs, and substituted into every consumer that builds encoder options.
|
||||
///
|
||||
/// <para>The rule that must never break: whatever the creator typed is what the encoder is asked to
|
||||
/// hold, and no tier's minimum is quietly undercut.</para>
|
||||
/// </summary>
|
||||
[Collection("MainViewModelLayoutPath")]
|
||||
public class RatedBitrateTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_creator_rate_is_per_tier_clamped_to_the_minimum_and_restored()
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-rate-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
try
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
var tier60 = vm.QualityOptions.Single(tier => tier.Display == "1080p60");
|
||||
var tier30 = vm.QualityOptions.Single(tier => tier.Display == "1080p30");
|
||||
vm.SelectedQuality = tier60;
|
||||
|
||||
// Unset means the shipped default, not zero.
|
||||
Assert.Equal(tier60.Bitrate, vm.RatedMbps);
|
||||
|
||||
// A creator who knows their line sets their own number, and the encoder sees it.
|
||||
vm.RatedMbps = 15;
|
||||
Assert.Equal(15, vm.RatedMbps);
|
||||
Assert.Equal(15, vm.EffectiveQuality(tier60).Bitrate);
|
||||
Assert.Equal(60, vm.EffectiveQuality(tier60).Fps); // geometry stays the tier's
|
||||
|
||||
// Below YouTube's minimum for the tier is not a legal target.
|
||||
vm.RatedMbps = 2;
|
||||
Assert.Equal(RateLadder.For(tier60).Minimum, vm.RatedMbps);
|
||||
|
||||
// The other tier keeps its own value — one setting does not leak into the ladder.
|
||||
vm.SelectedQuality = tier30;
|
||||
Assert.Equal(tier30.Bitrate, vm.RatedMbps);
|
||||
vm.SelectedQuality = tier60;
|
||||
|
||||
// And it survives a restart: the same profile, a fresh view model.
|
||||
var reloaded = new MainViewModel();
|
||||
reloaded.SelectedQuality = reloaded.QualityOptions.Single(tier => tier.Display == "1080p60");
|
||||
Assert.Equal(RateLadder.For(tier60).Minimum, reloaded.RatedMbps);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
try { File.Delete(tempDb); } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user