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));
}
}