fix(history): file each session under the real encoder, not "unknown"

FfmpegEncoder resolved the encoder name into its OWN StreamHealth instance, while the view
model read CurrentHealth — a different object. So every recorded session was filed as
"unknown", and the rate history's encoder key (evidence about NVENC says nothing about QSV)
had nothing to key on. The name now travels on the encoder interface and the pump forwards
it: IFfmpegEncoder.VideoEncoderName -> FramePump.EncoderName.

Caught while adding it: reading _framePump.EncoderName from RefreshRateProposal throws,
because that call runs at MainViewModel ctor line 354 and the pump is built at line 468.
Eight tests failed with the same NullReferenceException, which took a while to see because
the harness's test output is buffered — the first honest look at per-test logging named it
in one line. Guarded; the history falls back to the last session's encoder for that tier,
which is exactly the pre-first-session case the fallback already existed for.

Tests: FramePumpTests gains the encoder-name assertion (null before start, h264_nvenc
after), and the three fake encoders implement the new member.

675 pass, 0 warnings.
This commit is contained in:
2026-11-12 13:57:03 -08:00
parent 5ae52808bb
commit e654a16b60
9 changed files with 40 additions and 8 deletions
+4 -1
View File
@@ -29,6 +29,9 @@ public sealed class FfmpegEncoder : IFfmpegEncoder
private IEncoderProcess? _process;
private EncoderOptions? _options;
private StreamHealth _health = new() { Status = StreamStatus.Offline };
/// <inheritdoc/>
public string? VideoEncoderName { get; private set; }
private Task? _stderrLoop;
private bool _stopRequested;
@@ -82,7 +85,7 @@ public sealed class FfmpegEncoder : IFfmpegEncoder
var ffmpegPath = await _locator.LocateAsync(cancellationToken).ConfigureAwait(false);
var encoder = options.VideoEncoder ?? await ProbeEncoderAsync(ffmpegPath, cancellationToken).ConfigureAwait(false);
_health.VideoEncoder = encoder;
VideoEncoderName = encoder;
var args = FfmpegArgs.Build(options, encoder);
var startInfo = new ProcessStartInfo
+4
View File
@@ -91,6 +91,10 @@ public sealed class FramePump : IDisposable
private readonly object _gate = new();
private IFfmpegEncoder? _encoder;
/// <summary>The encoder this session is running, or null before the first start. The view model
/// files each broadcast's rate history under it.</summary>
public string? EncoderName => _encoder?.VideoEncoderName;
private CancellationTokenSource? _cts;
private Task? _pumpTask;
private bool _started;
+5
View File
@@ -19,6 +19,11 @@ public interface IFfmpegEncoder : IDisposable
/// <summary>Fires when the subprocess dies unexpectedly (non-zero exit while live).</summary>
event EventHandler<string>? ProcessFailed;
/// <summary>Which encoder the probe chose for this session ("h264_nvenc", "libopenh264", …). Null
/// before the first start. The rate history is keyed by it: evidence about one encoder says nothing
/// about another.</summary>
string? VideoEncoderName { get; }
Task StartAsync(EncoderOptions options, CancellationToken cancellationToken = default);
Task SubmitFrameAsync(VideoFrame frame, CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);