YouTube Live Chat Streaming
Overview
Lumio receives YouTube live chat messages via InnerTube — YouTube's own internal polling endpoint (youtubei/v1/live_chat/get_live_chat). This transport costs 0 Data API quota and delivers the full range of chat event types. gRPC streamList and REST polling are available as optional fallbacks but are disabled by default.
Architecture
Chat Transport
| Mode | Cost | Default |
|---|---|---|
| InnerTube (primary) | 0 quota | Always active |
| gRPC streamList (fallback) | ~0 quota | Disabled (grpc_fallback_enabled = false) |
| REST Data API v3 (fallback) | 5 units per poll | Disabled (rest_fallback_enabled = false) |
The worker starts every stream task in InnerTube mode. gRPC is lazy-connected only if enabled and InnerTube fails. REST polling is a last resort.
Fallback cascade: InnerTube → gRPC (if enabled) → REST (if enabled). Each transition triggers after 3 consecutive failures within 60 seconds.
How InnerTube Chat Works
InnerTube uses YouTube's own live chat polling contract:
- Bootstrap — The worker fetches the live chat embed page (
youtube.com/live_chat?v=\{video_id\}&is_popout=1) to extract acontinuationtoken and the current InnerTube API key. Both are cached in Redis so restarts don't re-scrape. - Poll loop —
get_live_chatis called with the continuation token. The response carries the next continuation token and a server-recommended polling interval. The worker honors that interval (minimum 1 s, default 3 s). - All event types flow through InnerTube — text messages, SuperChats, SuperStickers, memberships, gift memberships, message deletions, user bans, poll events, and members-only mode changes are all returned in the same response.
InnerTube is unauthenticated for chat reception — no OAuth token required.
Broadcast Discovery
Broadcast discovery runs independently of chat reception:
- Primary: InnerTube
browseendpoint — Queriesyoutubei.googleapis.com/youtubei/v1/browseto list live and upcoming broadcasts via the channel's Streams tab. 0 quota. Returns broadcast IDs, titles, viewer counts, and scheduled start times (but notliveChatId). - liveChatId resolution — For newly discovered broadcasts,
liveChatIdis resolved once via Data API v3liveBroadcasts.list(5 quota units). The result is cached in thechannel_statustable and survives worker restarts. Unresolvable broadcasts are skipped until they disappear and reappear. - Fallback: Data API v3 — If InnerTube browse fails entirely, the worker falls back to Data API discovery. Gated by
rest_fallback_enabled.
InnerTube client version: Auto-resolved by scraping youtube.com and cached in Redis (lumio:yt:innertube_version, 12 h TTL). The config value serves as a cold-boot fallback only.
Discovery intervals:
| State | Interval |
|---|---|
| Idle (no active broadcasts) | 60 s |
| Active (broadcasts running) | 60 s |
InnerTube costs 0 quota, so frequent polling is safe. Each cycle refreshes viewer counts, likes, total views, and detects new or ended broadcasts.
Badge + Emote Enrichment
Member badge images and channel-custom emotes are extracted inline from InnerTube chat responses — no separate observer is needed:
- Member badges — Each message's
authorBadgesarray is parsed for custom-thumbnail badges. The badge image URL and tier label are stored in theMembershipBadgeCache(LRU + Redis) and attached to rendered messages. - Channel emotes — Custom emojis with a
channelId/emoteIdpattern are extracted from emoji renderers and merged into the channel emote registry vialo_chat::merge_channel_emotes.
Broadcast Statistics
The worker fetches like counts and total view counts via InnerTube endpoints at 0 quota cost:
updated_metadataendpoint — Current like count per broadcastplayerendpoint — Lifetime view count per broadcast
These are stored in channel_status (like_count, total_views) and displayed on the dashboard and popout viewer badge.
Multi-Stream Support
YouTube allows multiple simultaneous live streams per channel. The worker manages them all:
- Discovery finds all active and upcoming broadcasts.
- A separate stream task is spawned per broadcast (each with its own InnerTube continuation state).
- Task lifecycle is managed per-broadcast: new → spawn, ended → cancel.
- Active streams are stored in Redis at
lumio:youtube:active_streams:\{account_id\}(TTL 120 s) for the frontend.
Messages from all streams appear together in the Multichat.
Configuration
All settings live in the [youtube] section of config/*.toml or as environment variables.
| Setting | ENV override | Default | Description |
|---|---|---|---|
grpc_fallback_enabled | LUMIO__YOUTUBE__GRPC_FALLBACK_ENABLED | false | Enable gRPC streamList as fallback when InnerTube fails |
rest_fallback_enabled | LUMIO__YOUTUBE__REST_FALLBACK_ENABLED | false | Enable REST polling as fallback when gRPC also fails; also gates Data API broadcast discovery fallback |
The one-time liveChatId resolution via Data API (liveBroadcasts.list) is always permitted regardless of rest_fallback_enabled.
Events Supported
Chat + Moderation
| Event type | Description |
|---|---|
TEXT_MESSAGE_EVENT | Regular chat message |
TOMBSTONE | Silent removal (no-op) |
MESSAGE_DELETED_EVENT | Message deleted — marked in DB, broadcast via WebSocket |
MESSAGE_RETRACTED_EVENT | Message retracted — same handling as deleted |
USER_BANNED_EVENT | User banned — moderation log entry with ban type and duration |
Monetization
| Event | Lumio event type | Key fields |
|---|---|---|
SUPER_CHAT_EVENT | youtube:superchat | amount_micros, currency, amount_display_string, user_comment, tier |
SUPER_STICKER_EVENT | youtube:supersticker | Same + sticker_id, alt_text |
Membership
| Event | Lumio event type | Key fields |
|---|---|---|
NEW_SPONSOR_EVENT | youtube:member | member_level_name, is_upgrade |
MEMBER_MILESTONE_CHAT_EVENT | youtube:member | member_level_name, member_month, user_comment |
MEMBERSHIP_GIFTING_EVENT | youtube:gift_membership | gift_memberships_count, gift_memberships_level_name |
GIFT_MEMBERSHIP_RECEIVED_EVENT | youtube:gift_membership_received | member_level_name, gifter_channel_id |
Gift memberships use the same bundling pattern as Twitch gift subs: the header gift_membership event collects recipient names from individual gift_membership_received events, then broadcasts with a giftRecipients list. Individual received events are stored in DB but excluded from the event panel via exclude_bundled.
Interactive
| Event | Lumio event type | Key fields |
|---|---|---|
POLL_EVENT | youtube:poll / youtube:poll_end | question_text, options[] (text + tally), status |
Polls are displayed in the ChatAlerts component using the same UI as Twitch polls.
Chat Sending + Moderation
Chat sending and moderation use the user's login OAuth connection (not the channel connection). InnerTube is read-only.
| Operation | Endpoint | Token source |
|---|---|---|
| Send message | POST /youtube/v3/liveChat/messages | get_provider_token("google") |
| Ban / timeout | POST /youtube/v3/liveChat/bans | get_provider_token("google") |
| Delete message | DELETE /youtube/v3/liveChat/messages?id=\{message_id\} | get_provider_token("google") |
All OAuth tokens are managed by the centralized Token Refresh Worker.
Quota Impact
| Operation | Units | Frequency |
|---|---|---|
| InnerTube chat polling | 0 | Per poll interval (~3 s per active stream) |
| InnerTube broadcast discovery | 0 | Every 60 s |
| InnerTube statistics (likes, views) | 0 | Every 60 s per broadcast |
| liveChatId resolution (Data API) | 5 | Once per broadcast, cached forever |
| REST broadcast discovery fallback | 1 | Every 60 s (only when InnerTube fails + rest_fallback_enabled) |
| Chat sending | 200 | Per message sent |
| Moderation action | 200 | Per action |
| Channel enrichment | 1 | On demand, cached |
Key Files
| File | Purpose |
|---|---|
crates/lo-youtube-api/src/innertube/mod.rs | InnerTube get_live_chat polling + API key/continuation bootstrap |
crates/lo-youtube-api/src/innertube/parser.rs | Parse get_live_chat responses into InnerTubeChatEvent variants |
crates/lo-youtube-api/src/innertube/events.rs | InnerTubeChatEvent enum definition |
crates/lo-youtube-api/src/innertube/browse.rs | InnerTube browse (broadcast discovery, viewer counts, likes) |
crates/lo-youtube-api/src/innertube/scraper.rs | Scrape API key + client version from youtube.com |
crates/lo-youtube-api/src/streaming.rs | gRPC streamList client (fallback) |
crates/lo-youtube-api/src/client.rs | REST Data API v3 client (fallback + moderation) |
apps/api/src/workers/youtube.rs | Multi-stream worker: broadcast discovery, per-stream task lifecycle, InnerTube chat loop |
apps/api/src/graphql/youtube.rs | youtubeActiveStreams GraphQL query |
apps/api/src/routes/youtube_streams.rs | GET /v1/youtube/active-streams REST endpoint |
apps/api/src/graphql/channel_status.rs | channelStatus query (includes broadcastStatus, likeCount, totalViews, scheduledStart, liveChatId) |
apps/web/src/hooks/use-youtube-streams.ts | Frontend hook polling active streams |
apps/web/src/app/(app)/dashboard/chat/multichat.tsx | Broadcast sub-menu + reply context |