fix: the broadcast diagnostic reported NO-BINDING from a parse that never ran

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.
This commit is contained in:
2026-10-04 15:05:28 -07:00
parent 0fb2f9c6c3
commit 8fbc3701d2
3 changed files with 68 additions and 8 deletions
+26 -8
View File
@@ -201,7 +201,14 @@ public class YouTubeStreamService
try
{
var broadcast = await GetJsonAsync($"liveBroadcasts?part=status,contentDetails,snippet&id={Uri.EscapeDataString(broadcastId)}");
var broadcastList = await GetJsonAsync($"liveBroadcasts?part=status,contentDetails,snippet&id={Uri.EscapeDataString(broadcastId)}");
// FIX 2026-10-04: liveBroadcasts.list returns a LIST wrapper, so the
// broadcast lives at items[0]. The first version of this diagnostic read
// the wrapper's root and reported every field as "?" — which then made
// BoundMatches report a confident "NO-BINDING" from a parse that never
// happened. A diagnostic that lies when it fails is worse than none.
var broadcast = FirstItem(broadcastList);
var read = broadcast.ValueKind == JsonValueKind.Object;
var status = Nested(broadcast, "status");
var details = Nested(broadcast, "contentDetails");
var snippet = Nested(broadcast, "snippet");
@@ -211,16 +218,12 @@ public class YouTubeStreamService
JsonElement? stream = string.IsNullOrWhiteSpace(streamId)
? null
: await GetJsonAsync($"liveStreams?part=status,cdn&id={Uri.EscapeDataString(streamId)}");
var streamItem = stream is { } present
&& present.TryGetProperty("items", out var streamItems)
&& streamItems.GetArrayLength() > 0
? streamItems[0]
: default(JsonElement);
var streamItem = stream is { } present ? FirstItem(present) : default(JsonElement);
AppLog.Write(
$"Broadcast diag: id={broadcastId} lifeCycle={lifeCycle} privacy={Text(status, "privacyStatus") ?? "?"} " +
$"recording={Text(status, "recordingStatus") ?? "?"} bound={bound ?? "NONE"} " +
$"expect={streamId ?? "none"} boundMatches={BoundMatches(bound, streamId)} " +
$"recording={Text(status, "recordingStatus") ?? "?"} bound={(read ? (bound ?? "NONE") : "UNREAD")} " +
$"expect={streamId ?? "none"} boundMatches={(read ? BoundMatches(bound, streamId) : "unread")} " +
$"boundLastUpdate={Text(details, "boundStreamLastUpdateTimeMs") ?? "?"} " +
$"autoStart={Flag(details, "enableAutoStart")} autoStop={Flag(details, "enableAutoStop")} " +
$"liveChatId={(string.IsNullOrWhiteSpace(Text(snippet, "liveChatId")) ? "none" : "present")} | " +
@@ -236,6 +239,21 @@ public class YouTubeStreamService
}
}
/// <summary>Descends into a `*.list` response and returns its single item. Every list
/// endpoint wraps the resource in `items[]`, so reading the response root reads
/// the wrapper and silently yields nothing — which is exactly the bug this
/// exists to prevent. Returns <c>default</c> when there is no item (empty list,
/// or a shape we do not recognise) so callers can tell "absent" from "present".</summary>
internal static JsonElement FirstItem(JsonElement listResponse)
{
return listResponse.ValueKind == JsonValueKind.Object
&& listResponse.TryGetProperty("items", out var items)
&& items.ValueKind == JsonValueKind.Array
&& items.GetArrayLength() > 0
? items[0]
: default;
}
/// <summary>The bound stream must be the one we are actually feeding. A mismatch is
/// the failure mode where ingest succeeds (the reusable stream happily accepts the
/// bytes) while the broadcast waits forever for a stream that is not sending.</summary>