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.
This commit is contained in:
2026-11-11 09:52:27 -08:00
parent 2c5a3d8c11
commit 24077e58cf
4 changed files with 34 additions and 9 deletions
+10 -3
View File
@@ -248,17 +248,24 @@ public sealed class FramePump : IDisposable
}
}
/// <summary>TASK 33: relaunch the pump and encoder at a new tier, over the same URL and audio pipe.
/// <summary>TASK 33: relaunch the pump and encoder at a new tier, over the same URL and named audio pipe.
///
/// <para>Full stop + start rather than an encoder swap, deliberately: <see cref="StopAsync"/> awaits
/// the loop and flushes the encoder, so no in-flight submit can land on a disposed process. A bare
/// swap races exactly that and would fault the loop — which ends the broadcast, the one thing a
/// quality step-down must never do. The cost is a short freeze, already accepted by the ladder.</para>
/// quality step-down must never do. The audio pipe must be closed before the pump stops and freshly
/// opened before it starts again; otherwise ffmpeg waits on the still-open audio input, then the new
/// process tries to attach to a broken server. The cost is a short freeze, already accepted by the ladder.</para>
/// </summary>
public async Task<bool> ChangeEncoderAsync(CancellationToken cancellationToken = default)
public async Task<bool> ChangeEncoderAsync(Action stopAudio, Action startAudio,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stopAudio);
ArgumentNullException.ThrowIfNull(startAudio);
stopAudio();
await StopAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested) return false;
startAudio();
await StartAsync(cancellationToken);
return IsRunning;
}