using System; using ytLive.Models; using ytLive.Services.Encoder; namespace ytLive.Services; /// /// TASK 33 slice 2: the session's adaptor between the pump's evidence and the pure policy. One planner /// per broadcast, because the policy is stateful (budget, cooldown, current tier) and a step must /// survive across windows. /// /// It deliberately does not act. It answers "should this session change something, and why"; the /// view model owns the restart, the toast and the session-report line. Keeping the effects out is what /// makes the whole ladder testable without an encoder running. /// public sealed class StepDownPlanner { private readonly StepDownPolicy _policy; private readonly DateTime _startedUtc; public StepDownPlanner(StepDownPolicy policy, DateTime startedUtc) { _policy = policy; _startedUtc = startedUtc; } public StepDownPolicy Policy => _policy; /// Current tier after any applied steps, in the shape the encoder restart needs. public (int Width, int Height, int Fps, double BitrateMbps) Current => (_policy.Width, _policy.Height, _policy.Fps, _policy.BitrateMbps); public StepDownDecision Observe(in FramePumpStats stats, DateTime nowUtc) { var sample = new StepDownSample( WindowSeconds: stats.WindowSeconds, PumpStalls: stats.Stalls, PumpDrops: 0, // the pump pads with the last frame; FramesRendered is ambiguous EncoderDrops: stats.Dropped, AvgRenderMs: stats.AvgRenderMs, AvgSubmitWaitMs: stats.AvgSubmitMs); return _policy.Observe(sample, nowUtc - _startedUtc); } /// The plain sentence the creator sees, chosen by cause. Never a mechanism string: "ffmpeg /// exited", "dropped 8", an HTTP code are log findings, not an explanation of what happened to them. /// public static string Describe(in StepDownDecision decision, (int Width, int Height, int Fps, double BitrateMbps) landed) => decision.Action switch { StepAction.LowerFrameRate => "LlamaCasty couldn't keep up with your scene, so it lowered the frame rate to " + $"{landed.Fps}. Your connection wasn't the problem.", StepAction.LowerResolution => "LlamaCasty couldn't keep up with your scene, so it lowered the picture to " + $"{landed.Width}×{landed.Height}. Your connection wasn't the problem.", StepAction.LowerBitrate => $"Your upload couldn't hold the bitrate, so LlamaCasty lowered it to {landed.BitrateMbps:0.0} Mbps " + "to stop the freezing. It will stay there for this stream.", StepAction.RaiseFrameRate or StepAction.RaiseResolution or StepAction.RaiseBitrate => "Conditions were clean for a while, so LlamaCasty restored one step of quality.", _ => string.Empty, }; /// One line for the session report, so a later question is answered from data. public static string ReportLine(DateTime whenLocal, in StepDownDecision decision) => decision.Action switch { StepAction.None => string.Empty, StepAction.LowerFrameRate or StepAction.LowerResolution => $"{whenLocal:HH:mm} quality stepped down — encoder ({decision.Why})", StepAction.LowerBitrate => $"{whenLocal:HH:mm} quality stepped down — upload ({decision.Why})", _ => $"{whenLocal:HH:mm} quality restored ({decision.Why})", }; }