Slice 2. 588 green, 0 warnings. - `FramePumpStats` and `FramePump.StatsReported`: the 5 s window the log line already describes, as data. `Dropped` is the ENCODER's counter (frames submitted, not accepted); `Stalls`/`AvgRenderMs` are the pump's. `FramesRendered` is deliberately not used for classification — a static scene reuses the paste cache and renders few frames while completely healthy, so it would read as starvation. - `StepDownPlanner`: one per broadcast, owns the stateful policy, converts stats → sample, exposes the landed tier for the restart. It never acts — the VM owns the effects, which is what keeps the ladder testable without an encoder. - `Describe` / `ReportLine`: the sentences a creator sees. Machine cause says LlamaCasty couldn't keep up and the connection was not the problem; upload cause blames the upload and promises no further thrash. A test asserts the machine copy never leaks "dropped" — mechanism strings stay in the log. Remaining for the task: VM subscription, the encoder restart at the landed tier, the toast, and shedding our own capture cost before stepping fps.
78 lines
3.6 KiB
C#
78 lines
3.6 KiB
C#
using System;
|
||
using ytLive.Models;
|
||
using ytLive.Services.Encoder;
|
||
|
||
namespace ytLive.Services;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
///
|
||
/// <para>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.</para>
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>Current tier after any applied steps, in the shape the encoder restart needs.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>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.
|
||
/// </summary>
|
||
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,
|
||
};
|
||
|
||
/// <summary>One line for the session report, so a later question is answered from data.</summary>
|
||
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})",
|
||
};
|
||
}
|