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:
2026-09-25 08:06:02 -07:00
parent d4aa588da0
commit 94f015044a
8 changed files with 219 additions and 83 deletions
@@ -183,36 +183,6 @@ public partial class MainViewModel : ViewModelBase
_notifications.Warning("Complete your broadcast details",
"Your title and description are empty — fill them in from the Text panel on the right.");
}
// Start chat polling (TASK 9.4): fetch the liveChatId from the broadcast's
// contentDetails, then start the poll loop. Chat is non-fatal — if it fails,
// the stream continues without on-panel chat.
string? liveChatId = null;
try
{
liveChatId = await _youtubeStream.GetBroadcastLiveChatIdAsync(_currentBroadcastId);
if (liveChatId != null)
{
_youtubeChat.Start(liveChatId);
AppLog.Write($"Chat polling started: {liveChatId}");
}
else
{
AppLog.Write("Could not retrieve liveChatId; chat polling disabled");
}
}
catch (Exception chatEx)
{
AppLog.Write($"Chat start failed (non-fatal): {chatEx.Message}");
}
// TASK 41: a test broadcast hands its session to the Test drawer once
// the liveChatId resolves — Mock Chat Input + simulated events need it.
if (IsTestStream && _currentBroadcastId != null)
{
TestSession.BeginTest(_currentBroadcastId, liveChatId);
AppLog.Write($"Test session dock open for {_currentBroadcastId}");
}
}
catch (Exception ex)
{
@@ -222,8 +192,40 @@ public partial class MainViewModel : ViewModelBase
return;
}
// The pump is what pushes RTMP; enableAutoStart flips the broadcast ready→live
// only once frames flow. The liveChatId (snippet.liveChatId) does not exist
// before that (2026-09-25 diagnosis), so chat resolution runs AFTER the pump
// starts. Chat is non-fatal — if it fails, the stream continues without chat.
await _framePump.StartAsync(); // never throws; failures log + surface via Failed
string? liveChatId = null;
try
{
if (_currentBroadcastId != null)
liveChatId = await _youtubeStream.GetBroadcastLiveChatIdAsync(_currentBroadcastId);
if (liveChatId != null)
{
_youtubeChat.Start(liveChatId);
AppLog.Write($"Chat polling started: {liveChatId}");
}
else
{
AppLog.Write("Could not retrieve liveChatId; chat polling disabled");
}
}
catch (Exception chatEx)
{
AppLog.Write($"Chat start failed (non-fatal): {chatEx.Message}");
}
// TASK 41: a test broadcast hands its session to the Test drawer once
// the liveChatId resolves — Mock Chat Input + simulated events need it.
if (IsTestStream && _currentBroadcastId != null)
{
TestSession.BeginTest(_currentBroadcastId, liveChatId);
AppLog.Write($"Test session dock open for {_currentBroadcastId}");
}
// Health polling (TASK 5 item 3): poll immediately, then every 30s while
// live. Report-by-exception — a banner appears only on warning/error.
_healthPollTimer.Start();