Last slice. 590 green, 0 warnings. - **Shed before you charge the viewer.** On machine evidence the policy's first act is `ShedCaptureCadence`: `ScreenCaptureFrameSource` widens its conversion interval 10 ms → 33 ms (≈100 → 30 conversions/s) through a new `ScreenCaptureManager.ShedLoad`. The output tier, the declared fps and the viewer's picture are untouched — we stop paying for readbacks we were throwing away. It costs no step budget; if the starvation persists, the fps ladder runs on the next clean window. - The shed is reset on every stop path, so a cheap session cannot leave a later one capped. - **The session line is a Logs-tab line, not a session-report line.** The session report is the money summary (Super Chats, per currency); quality chatter there would bury the numbers it exists for. The cause-tagged line already lands in the log at the moment of the step. TASK 33 is complete: two ladders, budgets, floors, ambiguity, one-way with recovery behind a flag, the relaunch, the copy, and the cost shed. 18 tests pin the ladders, the copy and the restart.
103 lines
4.0 KiB
C#
103 lines
4.0 KiB
C#
using System;
|
||
using System.Windows;
|
||
using ytLive.Helpers;
|
||
using ytLive.Models;
|
||
using ytLive.Services;
|
||
using ytLive.Services.Encoder;
|
||
|
||
namespace ytLive.ViewModels;
|
||
|
||
/// <summary>
|
||
/// TASK 33, slice 3: the session's step-down watch — policy feed, one encoder relaunch, one toast.
|
||
///
|
||
/// <para>Kept in its own file because it is the only place that both reads the pump's stats and acts on
|
||
/// the session; the pure decision lives in <see cref="StepDownPolicy"/>, the sentence in
|
||
/// <see cref="StepDownPlanner"/>.</para>
|
||
/// </summary>
|
||
public partial class MainViewModel
|
||
{
|
||
private StepDownPlanner? _stepDownPlanner;
|
||
private (int Width, int Height, int Fps, double BitrateMbps)? _encoderTierOverride;
|
||
private bool _stepDownBusy;
|
||
|
||
/// <summary>Starts the watch for this broadcast. Called once per session, before the pump starts.</summary>
|
||
private void StartStepDownWatch()
|
||
{
|
||
var quality = OutputProfile.Resolve(SelectedQuality, QualityOptions, HasProOutput);
|
||
_encoderTierOverride = null;
|
||
_stepDownPlanner = new StepDownPlanner(
|
||
new StepDownPolicy(quality.Width, quality.Height, quality.Fps, quality.Bitrate),
|
||
_sessionStartedAtUtc ?? DateTime.UtcNow);
|
||
_framePump.StatsReported += OnPumpStatsReported;
|
||
}
|
||
|
||
/// <summary>Ends the watch. Called from StopStream on every path, so a stale planner can never
|
||
/// carry a spent budget into the next broadcast.</summary>
|
||
private void StopStepDownWatch()
|
||
{
|
||
_framePump.StatsReported -= OnPumpStatsReported;
|
||
_stepDownPlanner = null;
|
||
_encoderTierOverride = null;
|
||
_stepDownBusy = false;
|
||
_screenCaptureManager.ShedLoad(false); // never carry a shed cadence into the next show
|
||
}
|
||
|
||
/// <summary>Runs on the PUMP thread, so it must never block: the restart it may trigger stops the
|
||
/// very loop that raised this event. Decide here (pure and fast), act on the dispatcher.</summary>
|
||
private void OnPumpStatsReported(object? sender, FramePumpStats stats)
|
||
{
|
||
if (_stepDownPlanner is not { } planner) return;
|
||
|
||
var decision = planner.Observe(stats, DateTime.UtcNow);
|
||
if (!decision.Acted) return;
|
||
|
||
var dispatcher = Application.Current?.Dispatcher;
|
||
if (dispatcher == null) return;
|
||
_ = dispatcher.InvokeAsync(() => ApplyStepDownAsync(decision));
|
||
}
|
||
|
||
private async void ApplyStepDownAsync(StepDownDecision decision)
|
||
{
|
||
var planner = _stepDownPlanner;
|
||
if (planner == null || _stepDownBusy) return;
|
||
|
||
_stepDownBusy = true;
|
||
var landed = planner.Current;
|
||
try
|
||
{
|
||
if (decision.Action == StepAction.ShedCaptureCadence)
|
||
{
|
||
_screenCaptureManager.ShedLoad(true);
|
||
AppLog.Write($"Capture load shed: {decision.Why}");
|
||
return;
|
||
}
|
||
|
||
_encoderTierOverride = landed;
|
||
AppLog.Write($"Quality step-down: {decision.Why}");
|
||
await _framePump.ChangeEncoderAsync();
|
||
|
||
// Show what is actually going out. The dropdown keeps the creator's choice — the policy
|
||
// does not get to rewrite their intent, only the encoder's output.
|
||
CurrentHealth.FPS = landed.Fps;
|
||
CurrentHealth.CurrentBitrate = landed.BitrateMbps;
|
||
ResolutionBadgeText = $"{landed.Width}×{landed.Height} @ {landed.Fps} fps · lowered";
|
||
OnPropertyChanged(nameof(CurrentHealth));
|
||
OnPropertyChanged(nameof(ResolutionBadgeText));
|
||
|
||
var copy = StepDownPlanner.Describe(decision, landed);
|
||
if (copy.Length > 0)
|
||
_notifications.Info("Quality lowered", copy);
|
||
|
||
AppLog.Write($"Quality step-down complete: {StepDownPlanner.ReportLine(DateTime.Now, decision)}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLog.Write($"Quality step-down failed: {ex.Message}", AppLogMessageType.Error, AppLogErrorClass.Class2);
|
||
}
|
||
finally
|
||
{
|
||
_stepDownBusy = false;
|
||
}
|
||
}
|
||
}
|