diff --git a/Models/StreamConfig.cs b/Models/StreamConfig.cs index 0a8c3c9..e0ddd6d 100644 --- a/Models/StreamConfig.cs +++ b/Models/StreamConfig.cs @@ -29,11 +29,6 @@ public class StreamHealth public TimeSpan StreamDuration { get; set; } public string? LastError { get; set; } - /// 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. - public string? VideoEncoder { get; set; } - // YouTube-specific (liveStreams.status) public string? HealthStatus { get; set; } // "good", "ok", "bad", "noData" public string? HealthMessage { get; set; } diff --git a/Services/Encoder/FfmpegEncoder.cs b/Services/Encoder/FfmpegEncoder.cs index 2df2af3..9705057 100644 --- a/Services/Encoder/FfmpegEncoder.cs +++ b/Services/Encoder/FfmpegEncoder.cs @@ -29,6 +29,9 @@ public sealed class FfmpegEncoder : IFfmpegEncoder private IEncoderProcess? _process; private EncoderOptions? _options; private StreamHealth _health = new() { Status = StreamStatus.Offline }; + + /// + 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 diff --git a/Services/Encoder/FramePump.cs b/Services/Encoder/FramePump.cs index d7f7c59..1d997fa 100644 --- a/Services/Encoder/FramePump.cs +++ b/Services/Encoder/FramePump.cs @@ -91,6 +91,10 @@ public sealed class FramePump : IDisposable private readonly object _gate = new(); private IFfmpegEncoder? _encoder; + + /// The encoder this session is running, or null before the first start. The view model + /// files each broadcast's rate history under it. + public string? EncoderName => _encoder?.VideoEncoderName; private CancellationTokenSource? _cts; private Task? _pumpTask; private bool _started; diff --git a/Services/Encoder/IFfmpegEncoder.cs b/Services/Encoder/IFfmpegEncoder.cs index 522251f..625f7b3 100644 --- a/Services/Encoder/IFfmpegEncoder.cs +++ b/Services/Encoder/IFfmpegEncoder.cs @@ -19,6 +19,11 @@ public interface IFfmpegEncoder : IDisposable /// Fires when the subprocess dies unexpectedly (non-zero exit while live). event EventHandler? ProcessFailed; + /// 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. + string? VideoEncoderName { get; } + Task StartAsync(EncoderOptions options, CancellationToken cancellationToken = default); Task SubmitFrameAsync(VideoFrame frame, CancellationToken cancellationToken = default); Task StopAsync(CancellationToken cancellationToken = default); diff --git a/ViewModels/MainViewModel.Rate.cs b/ViewModels/MainViewModel.Rate.cs index fbca674..8b6ed32 100644 --- a/ViewModels/MainViewModel.Rate.cs +++ b/ViewModels/MainViewModel.Rate.cs @@ -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); diff --git a/ViewModels/MainViewModel.StepDown.cs b/ViewModels/MainViewModel.StepDown.cs index 58b4085..c3980a0 100644 --- a/ViewModels/MainViewModel.StepDown.cs +++ b/ViewModels/MainViewModel.StepDown.cs @@ -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)); diff --git a/ytLive.Tests/BrandFlashOutputTests.cs b/ytLive.Tests/BrandFlashOutputTests.cs index 7ad5722..b7976a8 100644 --- a/ytLive.Tests/BrandFlashOutputTests.cs +++ b/ytLive.Tests/BrandFlashOutputTests.cs @@ -580,6 +580,8 @@ public sealed class BrandFlashOutputTests /// production encoder uses, so the assertion is about the bytes on the wire. private sealed class CapturingEncoder : IFfmpegEncoder { + public string? VideoEncoderName => "h264_nvenc"; + public readonly List Frames = new(); public readonly TaskCompletionSource FirstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/ytLive.Tests/EncoderRestartTests.cs b/ytLive.Tests/EncoderRestartTests.cs index d1f3d54..d635516 100644 --- a/ytLive.Tests/EncoderRestartTests.cs +++ b/ytLive.Tests/EncoderRestartTests.cs @@ -86,6 +86,8 @@ public sealed class EncoderRestartTests private sealed class CapturingEncoder : IFfmpegEncoder { + public string? VideoEncoderName => "h264_nvenc"; + public readonly TaskCompletionSource FirstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously); public event Action? Started; public Action? Stopped { get; set; } diff --git a/ytLive.Tests/FramePumpTests.cs b/ytLive.Tests/FramePumpTests.cs index 988e386..838b5a5 100644 --- a/ytLive.Tests/FramePumpTests.cs +++ b/ytLive.Tests/FramePumpTests.cs @@ -22,6 +22,8 @@ public class FramePumpTests { private sealed class FakeEncoder : IFfmpegEncoder { + public string? VideoEncoderName => "h264_nvenc"; + public readonly List Frames = new(); public readonly List 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(); + } + /// 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