The run I asked for came back with three alarming lines: lifeCycle=? privacy=? recording=? bound=NONE boundMatches=NO-BINDING autoStart=unset autoStop=unset liveChatId=none | streamStatus=active Every single broadcast field was empty, and the stream half was fine. That combination is the tell: not a failed request, a wrong nesting level. liveBroadcasts.list returns a LIST wrapper, so the broadcast is at items[0]. I descended into items for the stream and read the response root for the broadcast, so every lookup missed, and BoundMatches — handed a null it could not distinguish from a real null — reported a confident NO-BINDING. The diagnostic invented the bug it was written to find. Three lines of fabricated alarm on the one run that was supposed to produce evidence. Two fixes, and the second is the one that matters: FirstItem() descends into items[0] for both halves, and returns default when there is genuinely no item. The line now prints boundMatches=unread when the broadcast could not be read at all, so an absent parse degrades to "I don't know" instead of a verdict. A diagnostic that guesses is worse than no diagnostic: this one would have sent me off to rebuild a binding that may well be correct. Test guards the regression: FirstItem_DescendsIntoTheListWrapper_AndReports- AbsenceHonestly, including the empty-items and missing-items cases. Suite 390/390, 0 warnings. Credit to the run for catching it. What it did establish, from the half that worked: streamStatus goes ready -> active about 30s into the push. So the ingest registers as active server-side, which combined with the 99MB muxed yesterday means the stream half is verified healthy end to end. The broadcast half needs one more run to say anything.
59 lines
3.0 KiB
C#
59 lines
3.0 KiB
C#
using System.Text.Json;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Broadcast-binding diagnostics (2026-10-04). Two test streams proved the RTMP push
|
|
/// is healthy — ffmpeg delivered 101,229 KiB video + 1,419 KiB audio over 112 seconds
|
|
/// with no socket error — yet the broadcast never left `ready` and close-out returned
|
|
/// 403 `invalidTransition`. Google's docs say <c>enableAutoStart</c> fires "when you
|
|
/// start streaming video on the bound live stream", so the one thing worth asserting
|
|
/// about the request we send is that we know which stream we think we are bound to.
|
|
/// </summary>
|
|
public class BroadcastBindingTests
|
|
{
|
|
/// <summary>The comparison that decides whether the broadcast is even watching the
|
|
/// feed we are pushing. A reusable stream happily accepts bytes for an unbound
|
|
/// broadcast, so ingest succeeding proves nothing on its own — a MISMATCH or
|
|
/// NO-BINDING would look identical from the RTMP side and would hang forever.
|
|
/// "unknown" is the honest answer when the app has no stream id to compare against,
|
|
/// so a missing expectation is never silently reported as a match.</summary>
|
|
[Theory]
|
|
[InlineData("stream-abc", "stream-abc", "yes")]
|
|
[InlineData("stream-abc", "stream-xyz", "MISMATCH")]
|
|
[InlineData(null, "stream-abc", "NO-BINDING")]
|
|
[InlineData("", "stream-abc", "NO-BINDING")]
|
|
[InlineData("stream-abc", null, "unknown")]
|
|
[InlineData("stream-abc", "", "unknown")]
|
|
public void BoundMatches_ComparesTheBroadcastAgainstTheStreamWePush(string? bound, string? expected, string verdict)
|
|
{
|
|
Assert.Equal(verdict, YouTubeStreamService.BoundMatches(bound, expected));
|
|
}
|
|
|
|
/// <summary>The regression the first diagnostic shipped with. `liveBroadcasts.list`
|
|
/// returns a LIST wrapper, so the broadcast sits at `items[0]`; reading the response
|
|
/// root reads the wrapper, every field comes back empty, and the binding check then
|
|
/// reports a confident `NO-BINDING` from a parse that never happened. The first real
|
|
/// test run produced exactly that — three lines of fabricated alarm. `FirstItem` must
|
|
/// descend, and must return `default` (not throw, not an empty object) when there is
|
|
/// genuinely no item, so the caller can report "unread" instead of guessing.</summary>
|
|
[Theory]
|
|
[InlineData("{\"kind\":\"youtube#liveBroadcastListResponse\",\"items\":[{\"id\":\"bc1\",\"status\":{\"lifeCycleStatus\":\"ready\"}}]}", "bc1")]
|
|
[InlineData("{\"items\":[]}", null)]
|
|
[InlineData("{\"kind\":\"youtube#liveBroadcastListResponse\"}", null)]
|
|
public void FirstItem_DescendsIntoTheListWrapper_AndReportsAbsenceHonestly(string listJson, string? expectedId)
|
|
{
|
|
using var doc = JsonDocument.Parse(listJson);
|
|
var item = YouTubeStreamService.FirstItem(doc.RootElement);
|
|
|
|
if (expectedId == null)
|
|
{
|
|
Assert.Equal(JsonValueKind.Undefined, item.ValueKind);
|
|
return;
|
|
}
|
|
Assert.Equal(expectedId, item.GetProperty("id").GetString());
|
|
}
|
|
}
|