using ytLive.Models; namespace ytLive.ViewModels; /// /// 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. /// /// Why per tier: 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 substitutes. /// /// Why this matters now: with CBR holding the target (FfmpegArgs.AddRateControl) /// the number the creator types is the number that goes out, so it is a real quality choice rather /// than an average to undershoot. /// public partial class MainViewModel { private readonly Dictionary _ratedMbpsByTier = new(StringComparer.Ordinal); /// 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. 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; } /// The rate the selected tier will stream at, in Mbps. 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)); } } /// What YouTube expects for this tier, shown next to the field. 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."; } } /// Restores the shipped default for the selected tier. 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)); } }