d4aa588da0
Replace the external StreamElements feed with NATIVE YouTube events per the creator's 2026-09-23 question. Chat parity half: - YouTubeChatService decodes ALL SIX liveChat/messages event types into ChatMessage.Kind (superChat/superSticker/newSponsor/membershipGifting/ giftMembershipReceived/memberMilestoneChat) — the four formerly-empty overlay rows are real now — and re-arms its one-shot poll on the server's pollingIntervalMillis (streamList connection semantics; clamp 1000-6000ms, maxResults=2000). ParsePage internal static seam + ChatPage record for deterministic tests; optional HttpClient ctor seam kept; InjectSimulatedMessage (TASK 41) preserved. Alerts half (creator rulings: one celebration zone, six UNIQUE animations, no menus/polls, sub mention = chat row only, NO viewer count): - New SourceType.AlertBox 'Stream Alerts' (one per layout, CanAddAlerts gate mirroring chat; idle = transparent). - AlertRenderer: six distinct branded animations (SuperChat slide-up/shine/ count-up, SuperSticker scale-pop, NewMember drop-in/flash, MemberGift slide-left/ chip-fan, GiftReceived confetti, MemberMilestone rise/growth-bar), every card drawing the 'made with LlamaCasty!' brand line. - AlertOverlayLayer: true component (Commit-G pattern) — queue (cap 10, drop tail never stall), 33ms UI ticker, cache-first RenderFrame + UpdatePreview, Advance(double) as the deterministic test clock; ChatEventKind.None rows never enqueue. - Wired: resolver RenderAlertBox, _alertLayer ctor + dispose, LoadLayout previews, Add menu item (Controls/LeftPanel.xaml), TestSessionViewModel sims tagged (member->NewMember, superchat->SuperChat). Good Dog ONE integration test: AlertLayerTests (RealApp STA, real WPF raster) — ParsePage classifies all six kinds + cadence fields; None rows enqueue nothing; six events play pairwise-distinct moving frames then drain to null. Gate: clean build 0 warnings; full suite 316/317, the one failure (LayerReorderPersistenceTests.RealMouseDrag) repros on the clean tree — the known environmental class (real-mouse-drag no-ops with a game/fullscreen window focused). References (OBS/overlay ecosystem): - streamList semantics: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList - OBS alert-box pattern (designated celebration zone, idle transparent): creator-chosen model
74 lines
2.9 KiB
C#
74 lines
2.9 KiB
C#
namespace ytLive.Models;
|
|
|
|
public class YouTubeChannel
|
|
{
|
|
public string ChannelId { get; set; } = string.Empty;
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
public string ProfileImageUrl { get; set; } = string.Empty;
|
|
public string AccessToken { get; set; } = string.Empty;
|
|
public string RefreshToken { get; set; } = string.Empty;
|
|
public DateTime TokenExpiry { get; set; }
|
|
}
|
|
|
|
/// <summary>The channel's reusable live stream (TASK 5): created once, cached,
|
|
/// rebound to every broadcast. The RTMP URL the encoder pushes to is the
|
|
/// ingestion address + "/" + stream name.</summary>
|
|
public record ReusableStream(string Id, string IngestionAddress, string StreamName)
|
|
{
|
|
public string RtmpUrl => $"{IngestionAddress}/{StreamName}";
|
|
}
|
|
|
|
/// <summary>The six monetization events YouTube's live chat feed emits (the ONLY
|
|
/// native event stream there is — free subscribers produce no event). Each kind
|
|
/// drives its own celebration animation in the AlertBox. <c>None</c> = a normal
|
|
/// chat row: it never enqueues an alert (subscriber mentions stay chat rows).</summary>
|
|
public enum ChatEventKind
|
|
{
|
|
None,
|
|
SuperChat,
|
|
SuperSticker,
|
|
NewMember,
|
|
MemberGift,
|
|
GiftReceived,
|
|
MemberMilestone,
|
|
}
|
|
|
|
public class ChatMessage
|
|
{
|
|
public string Id { get; init; } = Guid.NewGuid().ToString();
|
|
public string AuthorName { get; set; } = string.Empty;
|
|
public string AuthorChannelId { get; set; } = string.Empty;
|
|
public string AuthorImageUrl { get; set; } = string.Empty;
|
|
public string Message { get; set; } = string.Empty;
|
|
public DateTime Timestamp { get; set; }
|
|
public bool IsSuperChat { get; set; }
|
|
public double SuperChatAmount { get; set; }
|
|
public string? SuperChatCurrency { get; set; }
|
|
|
|
/// <summary>YouTube's own pre-rendered amount ("$12.34") — preferred over
|
|
/// recomposing amount+currency ourselves, which mangles non-dollar locales.</summary>
|
|
public string? SuperChatDisplayString { get; set; }
|
|
|
|
public bool IsMember { get; set; }
|
|
public string? MembershipLevel { get; set; }
|
|
|
|
public ChatEventKind Kind { get; set; }
|
|
|
|
public string? SuperStickerDescription { get; set; }
|
|
|
|
/// <summary>membershipGiftingEvent: how many memberships were bought.</summary>
|
|
public int GiftNewMembersCount { get; set; }
|
|
|
|
/// <summary>giftMembershipReceivedEvent: who gifted this one.</summary>
|
|
public string? GifterName { get; set; }
|
|
|
|
/// <summary>memberMilestoneChatEvent: streak months (0 = YouTube didn't
|
|
/// expose a count — the animation falls back to the message text).</summary>
|
|
public int MilestoneMonths { get; set; }
|
|
|
|
/// <summary>TASK 41: the Test Stream dock injects synthetic member / SuperChat /
|
|
/// subscriber events locally (YouTube's insert API only supports text), so the
|
|
/// buffer can label them honestly — simulated events never reach YouTube.</summary>
|
|
public bool IsSimulated { get; set; }
|
|
}
|