fix(stream): TEST-tab chat — resolve liveChatId from snippet, poll until the broadcast is live
The open creator report ('I still cannot post a chat message in the TEST tab',
feedback 'Chat polling couldn't start — Mock Chat Input is disabled') was a single
cause: the liveChatId never resolved. Two YouTube API facts:
1. The id lives in snippet.liveChatId — contentDetails has no such property
(TASK 44 read part=contentDetails: could never resolve).
https://developers.google.com/youtube/v3/live/docs/liveBroadcasts
2. It only exists once the broadcast is LIVE — the official sample lists
broadcastStatus=active, and our fetch ran before the frame pump pushed RTMP
(enableAutoStart flips ready→live). A ready-state list legitimately returns
no id.
https://github.com/youtube/api-samples/blob/master/java/src/main/java/com/google/api/services/samples/youtube/cmdline/live/GetLiveChatId.java
Fix: GetBroadcastLiveChatIdAsync reads part=snippet and polls with a bounded
retry (10x/2s); PrepareAndStartLiveAsync starts the frame pump FIRST, then
resolves the id. Chat stays non-fatal. Docs ai.md/TASKS.md/HANDOFF.md +
MyMistakes recipe updated same commit. TASK 44.
Good Dog: ONE integration test (GetBroadcastLiveChatIdAsync_Polls_Snippet_...
) drives a broadcast that gains its id mid-retry and asserts every request used
part=snippet. Gate: clean build 0 warnings, vstest 318/318.
This commit is contained in:
@@ -339,16 +339,37 @@ public class YouTubeStreamService
|
||||
return $"YouTube rejected the message ({(int)response.StatusCode})";
|
||||
}
|
||||
|
||||
/// <summary>Returns the liveChatId for a broadcast by fetching its contentDetails.
|
||||
/// The liveChatId is needed to poll chat messages. Called after broadcast creation
|
||||
/// in PrepareAndStartLiveAsync.</summary>
|
||||
public async Task<string?> GetBroadcastLiveChatIdAsync(string broadcastId)
|
||||
/// <summary>Resolves the broadcast's liveChatId (needed to poll chat). Two facts
|
||||
/// pin this down (2026-09-25, from the official liveBroadcasts reference + the
|
||||
/// GetLiveChatId.java sample):
|
||||
/// 1. It lives under <c>snippet.liveChatId</c> — <c>contentDetails</c> has no such
|
||||
/// property, so part=contentDetails could NEVER resolve it (the "Chat polling
|
||||
/// couldn't start" report).
|
||||
/// 2. YouTube only populates it once the broadcast is <c>live</c> (the sample lists
|
||||
/// broadcastStatus=active) — fetching right after insert (lifecycleStatus
|
||||
/// <c>ready</c>) returns no id. So this polls with a bounded retry, which the
|
||||
/// caller must run AFTER the encoder starts pushing RTMP (enableAutoStart flips
|
||||
/// the broadcast to live). Returns the id or null after the retries exhaust;
|
||||
/// never throws.</summary>
|
||||
public async Task<string?> GetBroadcastLiveChatIdAsync(
|
||||
string broadcastId, int maxAttempts = 10, int delayMs = 2000)
|
||||
{
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
if (attempt > 0) await Task.Delay(delayMs);
|
||||
var liveChatId = await FetchLiveChatIdAsync(broadcastId);
|
||||
if (liveChatId != null) return liveChatId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> FetchLiveChatIdAsync(string broadcastId)
|
||||
{
|
||||
if (!await EnsureToken()) return null;
|
||||
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
|
||||
var response = await _http.GetAsync(
|
||||
$"{ApiBase}/liveBroadcasts?part=contentDetails&id={broadcastId}");
|
||||
$"{ApiBase}/liveBroadcasts?part=snippet&id={broadcastId}");
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
@@ -356,8 +377,8 @@ public class YouTubeStreamService
|
||||
var items = data.GetProperty("items");
|
||||
if (items.GetArrayLength() == 0) return null;
|
||||
|
||||
var contentDetails = items[0].GetProperty("contentDetails");
|
||||
if (contentDetails.TryGetProperty("liveChatId", out var liveChatId))
|
||||
var snippet = items[0].GetProperty("snippet");
|
||||
if (snippet.TryGetProperty("liveChatId", out var liveChatId))
|
||||
return liveChatId.GetString();
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user