using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Compositor; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 33 slice 3 — the step-down's one dangerous act: relaunching the encoder mid-broadcast. /// /// The restart is a full pump stop/start rather than an encoder swap, and this test is why: a swap /// leaves an in-flight submit pointing at a disposed process, which faults the loop and ends the /// broadcast. Here the pump must come back and produce a frame with a new encoder instance and /// the tier the policy landed on. /// public sealed class EncoderRestartTests { [Fact] public async Task ChangeEncoder_RelauchesWithANewEncoderAtTheNewTier() { var tier = (Width: 1920, Height: 1080, Fps: 60, Bitrate: 8.0); var started = new List(); var frames = new List(); var scene = new Scene { Name = "Live" }; scene.Elements.Add(new Source { Name = "Background", Type = SourceType.Background, IsBackground = true }); var black = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]); using var pump = new FramePump( sceneProvider: () => scene, frameResolver: _ => black, compositorOptions: () => new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }, encoderOptions: () => new EncoderOptions { RtmpUrl = "rtmp://a.rtmp.youtube.com/live2/abc", Width = tier.Width, Height = tier.Height, Fps = tier.Fps, BitrateKbps = (int)(tier.Bitrate * 1000), }, encoderFactory: () => { var encoder = new CapturingEncoder(); encoder.Started += options => started.Add(options); frames.Add(encoder); return encoder; }, log: _ => { }, pacingDelay: (_, _) => Task.Delay(5)); await pump.StartAsync(); Assert.True(await frames[0].FirstFrame.Task.WaitAsync(TimeSpan.FromSeconds(5)), "first session never produced a frame"); // The policy lands on 1080p30 @ 6.4 Mbps; the VM has already written the override, so the // options func now reports the new tier — which the restart must re-read. tier = (1920, 1080, 30, 6.4); Assert.True(await pump.ChangeEncoderAsync()); Assert.True(await frames[1].FirstFrame.Task.WaitAsync(TimeSpan.FromSeconds(5)), "restarted session never produced a frame"); Assert.Equal(2, started.Count); Assert.Equal(60, started[0].Fps); Assert.Equal(30, started[1].Fps); Assert.Equal(6400, started[1].BitrateKbps); Assert.NotSame(frames[0], frames[1]); await pump.StopAsync(); } private sealed class CapturingEncoder : IFfmpegEncoder { public readonly TaskCompletionSource FirstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously); public event Action? Started; public int DroppedFrames => 0; public event EventHandler? HealthUpdated { add { } remove { } } public event EventHandler? ProcessFailed { add { } remove { } } public Task StartAsync(EncoderOptions options, CancellationToken cancellationToken = default) { Started?.Invoke(options); return Task.CompletedTask; } public Task SubmitFrameAsync(VideoFrame frame, CancellationToken cancellationToken = default) { FirstFrame.TrySetResult(true); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Dispose() { } } }