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.
90 lines
3.8 KiB
C#
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));
|
|
}
|
|
}
|