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:
@@ -29,11 +29,6 @@ public class StreamHealth
|
||||
public TimeSpan StreamDuration { get; set; }
|
||||
public string? LastError { get; set; }
|
||||
|
||||
/// <summary>The encoder the probe actually chose ("h264_nvenc", "libopenh264", …). Filled once the
|
||||
/// encoder starts, and the key the rate history is filed under — evidence about NVENC says nothing
|
||||
/// about QSV on another machine.</summary>
|
||||
public string? VideoEncoder { get; set; }
|
||||
|
||||
// YouTube-specific (liveStreams.status)
|
||||
public string? HealthStatus { get; set; } // "good", "ok", "bad", "noData"
|
||||
public string? HealthMessage { get; set; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -145,7 +145,10 @@ public partial class MainViewModel
|
||||
// Before this run's first stream the encoder is unknown; the last session on this tier is
|
||||
// the honest guess, and evidence about a different encoder stays out either way.
|
||||
var lastForTier = history.LastOrDefault(entry => entry.Tier == tier.Display);
|
||||
var encoder = CurrentHealth.VideoEncoder
|
||||
// The pump is built later in the constructor than this runs, so before the first session
|
||||
// there is no encoder to ask; the last session on this tier is the honest guess, and
|
||||
// evidence about a different encoder stays out either way.
|
||||
var encoder = (_framePump is { } pump ? pump.EncoderName : null)
|
||||
?? (string.IsNullOrEmpty(lastForTier.Encoder) ? null : lastForTier.Encoder);
|
||||
|
||||
var suggestion = RateHistory.Suggest(history, tier.Display, encoder, RatedMbps);
|
||||
|
||||
@@ -57,7 +57,7 @@ public partial class MainViewModel
|
||||
{
|
||||
_layoutStore.AppendRateHistory(new SessionRateResult(
|
||||
SelectedQuality.Display,
|
||||
CurrentHealth.VideoEncoder ?? "unknown",
|
||||
_framePump.EncoderName ?? "unknown",
|
||||
_sessionRatedMbps,
|
||||
planner.Current.BitrateMbps,
|
||||
planner.Policy.StepsUsed));
|
||||
|
||||
@@ -580,6 +580,8 @@ public sealed class BrandFlashOutputTests
|
||||
/// production encoder uses, so the assertion is about the bytes on the wire.</summary>
|
||||
private sealed class CapturingEncoder : IFfmpegEncoder
|
||||
{
|
||||
public string? VideoEncoderName => "h264_nvenc";
|
||||
|
||||
public readonly List<VideoFrame> Frames = new();
|
||||
public readonly TaskCompletionSource FirstFrame =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
@@ -86,6 +86,8 @@ public sealed class EncoderRestartTests
|
||||
|
||||
private sealed class CapturingEncoder : IFfmpegEncoder
|
||||
{
|
||||
public string? VideoEncoderName => "h264_nvenc";
|
||||
|
||||
public readonly TaskCompletionSource<bool> FirstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public event Action<EncoderOptions>? Started;
|
||||
public Action? Stopped { get; set; }
|
||||
|
||||
@@ -22,6 +22,8 @@ public class FramePumpTests
|
||||
{
|
||||
private sealed class FakeEncoder : IFfmpegEncoder
|
||||
{
|
||||
public string? VideoEncoderName => "h264_nvenc";
|
||||
|
||||
public readonly List<VideoFrame> Frames = new();
|
||||
public readonly List<byte[]> Backings = new();
|
||||
public int StartCount;
|
||||
@@ -133,6 +135,22 @@ public class FramePumpTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_pump_reports_which_encoder_the_session_runs()
|
||||
{
|
||||
// The rate history is keyed by encoder: evidence recorded against NVENC must not be applied to a
|
||||
// machine that later runs QSV. Before this the name went to a StreamHealth copy nobody read, so
|
||||
// every filed session said "unknown".
|
||||
var encoder = new FakeEncoder();
|
||||
using var pump = NewPump(encoder);
|
||||
|
||||
Assert.Null(pump.EncoderName); // nothing has started yet
|
||||
await pump.StartAsync();
|
||||
Assert.Equal("h264_nvenc", pump.EncoderName);
|
||||
|
||||
await pump.StopAsync();
|
||||
}
|
||||
|
||||
/// <summary>The ONE integration test for TASK 5: the real service (hermetic
|
||||
/// HTTP) returns the reusable stream's RTMP URL, the provider seam hands it
|
||||
/// to the pump, and the pump starts the encoder with that exact URL — the
|
||||
|
||||
Reference in New Issue
Block a user