Files
LlamaCasty/ViewModels/MainViewModel.StepDown.cs
T
gramps 24077e58cf fix(stepdown): restart the audio pipe around encoder changes
A quality step-down now closes the named audio pipe before stopping/flushing the
pump, then opens a fresh server before restarting the pump. This prevents ffmpeg
from waiting on the old audio input and avoids reconnecting to a broken pipe.

EncoderRestartTests pins the audio-stop → video-stop → audio-start → video-start
order. 616 tests green, clean build with 0 warnings.
2026-11-11 09:52:27 -08:00

105 lines
4.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
_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;
}
}
}