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.
105 lines
4.1 KiB
C#
105 lines
4.1 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(
|
||
_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;
|
||
}
|
||
}
|
||
}
|