using System;
using System.Windows;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.ViewModels;
///
/// TASK 33, slice 3: the session's step-down watch — policy feed, one encoder relaunch, one toast.
///
/// 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 , the sentence in
/// .
///
public partial class MainViewModel
{
private StepDownPlanner? _stepDownPlanner;
private (int Width, int Height, int Fps, double BitrateMbps)? _encoderTierOverride;
private bool _stepDownBusy;
/// Starts the watch for this broadcast. Called once per session, before the pump starts.
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;
}
/// Ends the watch. Called from StopStream on every path, so a stale planner can never
/// carry a spent budget into the next broadcast.
private void StopStepDownWatch()
{
_framePump.StatsReported -= OnPumpStatsReported;
_stepDownPlanner = null;
_encoderTierOverride = null;
_stepDownBusy = false;
_screenCaptureManager.ShedLoad(false); // never carry a shed cadence into the next show
}
/// 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.
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(
_audioMixer.StopLive,
() => _audioMixer.StartLive(InstanceProfile.AudioPipeName(EncoderOptions.DefaultAudioPipeName)));
// 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;
}
}
}