feat(33): the step-down acts — one encoder relaunch, one toast, same URL
Slice 3. 589 green, 0 warnings. - **`FramePump.ChangeEncoderAsync`** is a full pump stop/start, not an encoder swap. A swap leaves an in-flight submit pointing at a disposed process, and that faults the loop — which ends the broadcast, the one thing a quality step-down must never do. `StopAsync` already awaits the loop and flushes, so this reuses the tested path and costs one short freeze, which the ladder accepts. - **`MainViewModel.StepDown`**: one planner per broadcast, subscribed to `StatsReported`, unsubscribed on every stop path. The pump thread only decides; the restart is posted to the dispatcher, because the act stops the loop that raised the event. - **The creator's choice is not rewritten**: `_encoderTierOverride` changes what the encoder emits while the dropdown keeps showing what they picked, and the badge adds `· lowered` so the two never disagree silently. - Copy comes from `StepDownPlanner.Describe` (machine cause owns it, upload cause blames the upload); the log gets the cause-tagged line for support. Test: `EncoderRestartTests` runs a real pump with a fake encoder, changes the tier, and asserts a NEW encoder started at the new fps/bitrate and produced a frame — the regression that would otherwise ship as "it lowered the badge and the stream died". Remaining for 33: shed our own capture cost before the fps step, and the session-report line.
This commit is contained in:
@@ -248,6 +248,21 @@ public sealed class FramePump : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>TASK 33: relaunch the pump and encoder at a new tier, over the same URL and 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>
|
||||
/// </summary>
|
||||
public async Task<bool> ChangeEncoderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await StopAsync(cancellationToken);
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
await StartAsync(cancellationToken);
|
||||
return IsRunning;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
IFfmpegEncoder? encoder;
|
||||
|
||||
Reference in New Issue
Block a user