Files
LlamaCasty/ViewModels/MainViewModel.Rate.cs
T
gramps f2cab5ca0f 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.
2026-11-12 08:11:13 -08:00

90 lines
3.8 KiB
C#

using ytLive.Models;
namespace ytLive.ViewModels;
/// <summary>
/// The creator's own video rate, per tier — the latitude the tier table deliberately does not take
/// away. A creator who knows their upload can set any rate from the tier's YouTube minimum upward;
/// nothing here invents a ceiling, because a wrong guess is what live step-down exists to catch.
///
/// <para><b>Why per tier:</b> a number that is sensible at 1080p60 is wasteful at 720p30, and the
/// creator who tuned one tier has not tuned the other. Unset means "use the shipped default", which
/// is what <see cref="EffectiveQuality"/> substitutes.</para>
///
/// <para><b>Why this matters now:</b> with CBR holding the target (<c>FfmpegArgs.AddRateControl</c>)
/// the number the creator types is the number that goes out, so it is a real quality choice rather
/// than an average to undershoot.</para>
/// </summary>
public partial class MainViewModel
{
private readonly Dictionary<string, double> _ratedMbpsByTier = new(StringComparer.Ordinal);
/// <summary>The tier with the creator's rate substituted for the shipped default. Every consumer
/// that builds encoder options or a step-down policy must use this, never the raw tier.</summary>
public QualityOption EffectiveQuality(QualityOption tier)
=> RatedFor(tier) is var rated && Math.Abs(rated - tier.Bitrate) > 0.001
? tier with { Bitrate = rated }
: tier;
private double RatedFor(QualityOption tier)
{
if (_ratedMbpsByTier.TryGetValue(tier.Display, out var cached)) return cached;
// The constructor resolves the default tier before the layout store exists, so the first read
// must not cache: a cached default would shadow the creator's stored rate for that run.
var stored = _layoutStore is null ? null : _layoutStore.LoadRatedMbps(tier.Display);
var value = Math.Max(stored ?? tier.Bitrate, RateLadder.For(tier).Minimum);
if (_layoutStore is not null) _ratedMbpsByTier[tier.Display] = value;
return value;
}
/// <summary>The rate the selected tier will stream at, in Mbps.</summary>
public double RatedMbps
{
get => RatedFor(SelectedQuality);
set
{
var tier = SelectedQuality;
var band = RateLadder.For(tier);
// Below the tier minimum YouTube will not hold the resolution; above ~100 is not a stream.
var clamped = Math.Clamp(Math.Round(value, 1), band.Minimum, 100.0);
if (Math.Abs(clamped - RatedFor(tier)) < 0.05)
{
OnPropertyChanged(nameof(RatedMbps));
return;
}
_ratedMbpsByTier[tier.Display] = clamped;
_layoutStore.SaveRatedMbps(tier.Display, clamped);
ApplyStreamQuality(tier);
OnPropertyChanged(nameof(RatedMbps));
OnPropertyChanged(nameof(RateHint));
}
}
/// <summary>What YouTube expects for this tier, shown next to the field.</summary>
public string RateHint
{
get
{
var band = RateLadder.For(SelectedQuality);
return $"YouTube recommends {band.Recommended:0.#} Mbps for {SelectedQuality.Display}; " +
$"it will not take less than {band.Minimum:0.#}. Your connection must carry this " +
"plus ~20% headroom.";
}
}
/// <summary>Restores the shipped default for the selected tier.</summary>
public void ResetRatedMbps()
{
var tier = SelectedQuality;
if (!_ratedMbpsByTier.ContainsKey(tier.Display)) return;
_ratedMbpsByTier.Remove(tier.Display);
_layoutStore.SaveRatedMbps(tier.Display, null);
_ratedMbpsByTier[tier.Display] = RatedFor(tier);
ApplyStreamQuality(tier);
OnPropertyChanged(nameof(RatedMbps));
OnPropertyChanged(nameof(RateHint));
}
}