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:
@@ -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>
|
||||
|
||||
@@ -1710,6 +1710,23 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
the whole experiment.** Guarded by `BroadcastBindingTests.BoundMatches_ComparesTheBroadcastAgainstTheStreamWePush`,
|
||||
which pins `unknown` for a missing expectation so an absent stream id can never be misreported
|
||||
as a match.
|
||||
- **⚠️ The first version of this diagnostic LIED, and the first real run caught it (2026-10-04).**
|
||||
`liveBroadcasts.list` returns a **list wrapper**, so the broadcast lives at `items[0]`. The
|
||||
original code descended into `items` for the *stream* but read the *broadcast's* response root,
|
||||
so every broadcast field came back `?` — and `BoundMatches` then dutifully reported a confident
|
||||
**`boundMatches=NO-BINDING`** from a parse that had never happened. Three lines of fabricated
|
||||
alarm. Two fixes, both load-bearing: **`FirstItem()`** descends into `items[0]` for both halves
|
||||
(and returns `default` when there is genuinely no item), and the line now prints
|
||||
**`boundMatches=unread`** when the broadcast could not be read, so an absent parse can never be
|
||||
reported as a verdict. Guarded by
|
||||
`BroadcastBindingTests.FirstItem_DescendsIntoTheListWrapper_AndReportsAbsenceHonestly`.
|
||||
**Generalisation: a diagnostic must degrade to "I don't know", never to a confident wrong
|
||||
answer** — otherwise one level of JSON nesting manufactures a bug that isn't there.
|
||||
- **What the one run that *did* work proved:** the stream half was correct, and it shows
|
||||
`streamStatus=ready` → **`streamStatus=active`** about 30s into the push. So the ingest does
|
||||
register as active server-side, which is a genuine step forward — combined with the 99MB muxed
|
||||
in the earlier run, the stream half of the system is now verified healthy. The broadcast half is
|
||||
still unobserved pending a re-run.
|
||||
- **Visibility picker (TASK 9 item 6) — DELIBERATE TEST-PHASE LOCK, not drift (2026-09-01).**
|
||||
The "always Private" enforcement in `YouTubeStreamService.CreateBroadcast` stays while the creator
|
||||
runs multi-month real-world testing: non-private test streams would clutter the channel with VODs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
|
||||
@@ -30,4 +31,28 @@ public class BroadcastBindingTests
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user