TASK 5 items 1-3: reusable stream, health banner, docs refresh
- Reusable stream: GetOrCreateReusableStreamAsync, cached via LayoutStore, bound at broadcast insert - Health monitoring: GetStreamHealthAsync polls liveStreams.status, report-by-exception banner - Go-live order: ensure stream → create broadcast → then start pump (URL must exist first) - Full broadcast form scoped (TASK 5 items 6-7) in TASKS.md + ai.md - TASK 23 trimmed to technical scope, TASK 26 added (marcom/launch kit) - Confidential files: MONETIZATION.md + MARCOM.md (gitignored, not committed) - .gitignore updated for confidential business files - HANDOFF.md updated with session state and next steps
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
@@ -99,6 +101,65 @@ public class FramePumpTests
|
||||
$"pixel ({x},{y}): expected rgb({r},{g},{b}), got rgb({br},{bg},{bb})");
|
||||
}
|
||||
|
||||
private sealed class FakeHttpHandler : HttpMessageHandler
|
||||
{
|
||||
public FakeHttpHandler(string body) => Body = body;
|
||||
|
||||
public string Body;
|
||||
public string? LastUri;
|
||||
public int RequestCount;
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
RequestCount++;
|
||||
LastUri = request.RequestUri?.ToString();
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(Body, Encoding.UTF8, "application/json"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// chain that makes go-live actually push. The stream is reused (listed),
|
||||
/// never recreated (no POST).</summary>
|
||||
[Fact]
|
||||
public async Task ReusableStream_Url_From_Service_Feeds_Encoder_Startup()
|
||||
{
|
||||
var handler = new FakeHttpHandler(
|
||||
"""
|
||||
{"items":[{"id":"S456","cdn":{"isReusable":true,"ingestionInfo":{"ingestionAddress":"rtmp://a.rtmp.youtube.com/live2","streamName":"KEY123"}}}]}
|
||||
""");
|
||||
var auth = new YouTubeAuthService("test-id", "test-secret");
|
||||
auth.SetSession(new YouTubeChannel
|
||||
{
|
||||
AccessToken = "acc-123",
|
||||
TokenExpiry = DateTime.UtcNow.AddHours(1),
|
||||
});
|
||||
var service = new YouTubeStreamService(auth, new HttpClient(handler));
|
||||
|
||||
var url = (await service.GetOrCreateReusableStreamAsync())?.RtmpUrl;
|
||||
var encoder = new FakeEncoder();
|
||||
using var pump = NewPump(encoder, options: () => url == null
|
||||
? null
|
||||
: new EncoderOptions { RtmpUrl = url, Width = 64, Height = 48, Fps = 60 });
|
||||
|
||||
await pump.StartAsync();
|
||||
|
||||
Assert.Equal("rtmp://a.rtmp.youtube.com/live2/KEY123", url);
|
||||
Assert.Equal(1, encoder.StartCount);
|
||||
Assert.Equal("rtmp://a.rtmp.youtube.com/live2/KEY123", encoder.LastOptions?.RtmpUrl);
|
||||
Assert.Equal(1, handler.RequestCount); // reuse path: list only, no insert
|
||||
Assert.NotNull(handler.LastUri);
|
||||
Assert.Contains("liveStreams", handler.LastUri);
|
||||
Assert.Contains("mine=true", handler.LastUri);
|
||||
|
||||
await pump.StopAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_CompositesScene_FeedsEncoder_StopsCleanly()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user