TASK 9.4: live chat wiring — right panel with Super Chat + membership badges

- YouTubeStreamService: added GetBroadcastLiveChatIdAsync to fetch liveChatId from broadcast contentDetails
- YouTubeChatService.Poll(): parse snippet.type to handle textMessageEvent, superChatEvent, newSponsorEvent
- MainViewModel: wire _youtubeChat.Start(liveChatId) in PrepareAndStartLiveAsync after broadcast creation
- MainViewModel: wire _youtubeChat.Stop() in StopStream before encoder stop
- MainViewModel: wire _youtubeChat.Dispose() in Shutdown
- MainWindow.xaml: updated chat DataTemplate with member badge (⭐), Super Chat highlight (💰 + gold border), timestamps (HH:mm)
- MainWindow.xaml.cs: auto-scroll ChatListBox on new messages via CollectionChanged
- Chat lifecycle is non-fatal: failures log and the stream continues without on-panel chat
This commit is contained in:
2026-08-19 11:59:29 -07:00
parent e97406d649
commit 1613acadc3
5 changed files with 182 additions and 10 deletions
+42 -2
View File
@@ -73,6 +73,7 @@ public class YouTubeChatService : IDisposable
{
var snippet = item.GetProperty("snippet");
var author = item.GetProperty("authorDetails");
var type = snippet.GetProperty("type").GetString() ?? "textMessageEvent";
var message = new ChatMessage
{
@@ -80,11 +81,50 @@ public class YouTubeChatService : IDisposable
AuthorName = author.GetProperty("displayName").GetString()!,
AuthorChannelId = author.GetProperty("channelId").GetString()!,
AuthorImageUrl = author.GetProperty("profileImageUrl").GetString()!,
Message = snippet.GetProperty("textMessageDetails").GetProperty("messageText").GetString()!,
Timestamp = DateTime.Parse(snippet.GetProperty("publishedAt").GetString()!),
IsMember = author.GetProperty("isChatSponsor").GetBoolean()
IsMember = author.GetProperty("isChatSponsor").GetBoolean(),
MembershipLevel = author.TryGetProperty("badgeText", out var badgeText)
? badgeText.GetString()
: null
};
switch (type)
{
case "superChatEvent":
message.IsSuperChat = true;
if (snippet.TryGetProperty("superChatDetails", out var scDetails))
{
message.SuperChatAmount = scDetails.TryGetProperty("amountMicros", out var amt)
? amt.GetDouble() / 1_000_000.0
: 0;
message.SuperChatCurrency = scDetails.TryGetProperty("currency", out var cur)
? cur.GetString()
: null;
message.Message = scDetails.TryGetProperty("userComment", out var comment)
? comment.GetString() ?? string.Empty
: string.Empty;
}
break;
case "newSponsorEvent":
message.Message = $"🎉 {message.AuthorName} became a member!";
if (snippet.TryGetProperty("membershipDetails", out var memDetails))
{
message.MembershipLevel = memDetails.TryGetProperty("memberLevelName", out var level)
? level.GetString()
: message.MembershipLevel;
}
break;
default: // textMessageEvent
message.Message = snippet.TryGetProperty("textMessageDetails", out var textDetails)
? textDetails.TryGetProperty("messageText", out var text)
? text.GetString() ?? string.Empty
: string.Empty
: string.Empty;
break;
}
MessageReceived?.Invoke(message);
}
}
+24
View File
@@ -190,4 +190,28 @@ public class YouTubeStreamService
}
return health;
}
/// <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)
{
if (!await EnsureToken()) return null;
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
var response = await _http.GetAsync(
$"{ApiBase}/liveBroadcasts?part=contentDetails&id={broadcastId}");
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(json);
var items = data.GetProperty("items");
if (items.GetArrayLength() == 0) return null;
var contentDetails = items[0].GetProperty("contentDetails");
if (contentDetails.TryGetProperty("liveChatId", out var liveChatId))
return liveChatId.GetString();
return null;
}
}