Skip to main content

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

ModeCostDefault
InnerTube (primary)0 quotaAlways active
gRPC streamList (fallback)~0 quotaDisabled (grpc_fallback_enabled = false)
REST Data API v3 (fallback)5 units per pollDisabled (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:

  1. Bootstrap — The worker fetches the live chat embed page (youtube.com/live_chat?v=\{video_id\}&is_popout=1) to extract a continuation token and the current InnerTube API key. Both are cached in Redis so restarts don't re-scrape.
  2. Poll loopget_live_chat is 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).
  3. 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:

  1. Primary: InnerTube browse endpoint — Queries youtubei.googleapis.com/youtubei/v1/browse to list live and upcoming broadcasts via the channel's Streams tab. 0 quota. Returns broadcast IDs, titles, viewer counts, and scheduled start times (but not liveChatId).
  2. liveChatId resolution — For newly discovered broadcasts, liveChatId is resolved once via Data API v3 liveBroadcasts.list (5 quota units). The result is cached in the channel_status table and survives worker restarts. Unresolvable broadcasts are skipped until they disappear and reappear.
  3. 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:

StateInterval
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 authorBadges array is parsed for custom-thumbnail badges. The badge image URL and tier label are stored in the MembershipBadgeCache (LRU + Redis) and attached to rendered messages.
  • Channel emotes — Custom emojis with a channelId/emoteId pattern are extracted from emoji renderers and merged into the channel emote registry via lo_chat::merge_channel_emotes.

Broadcast Statistics

The worker fetches like counts and total view counts via InnerTube endpoints at 0 quota cost:

  • updated_metadata endpoint — Current like count per broadcast
  • player endpoint — 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:

  1. Discovery finds all active and upcoming broadcasts.
  2. A separate stream task is spawned per broadcast (each with its own InnerTube continuation state).
  3. Task lifecycle is managed per-broadcast: new → spawn, ended → cancel.
  4. 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.

SettingENV overrideDefaultDescription
grpc_fallback_enabledLUMIO__YOUTUBE__GRPC_FALLBACK_ENABLEDfalseEnable gRPC streamList as fallback when InnerTube fails
rest_fallback_enabledLUMIO__YOUTUBE__REST_FALLBACK_ENABLEDfalseEnable 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 typeDescription
TEXT_MESSAGE_EVENTRegular chat message
TOMBSTONESilent removal (no-op)
MESSAGE_DELETED_EVENTMessage deleted — marked in DB, broadcast via WebSocket
MESSAGE_RETRACTED_EVENTMessage retracted — same handling as deleted
USER_BANNED_EVENTUser banned — moderation log entry with ban type and duration

Monetization

EventLumio event typeKey fields
SUPER_CHAT_EVENTyoutube:superchatamount_micros, currency, amount_display_string, user_comment, tier
SUPER_STICKER_EVENTyoutube:superstickerSame + sticker_id, alt_text

Membership

EventLumio event typeKey fields
NEW_SPONSOR_EVENTyoutube:membermember_level_name, is_upgrade
MEMBER_MILESTONE_CHAT_EVENTyoutube:membermember_level_name, member_month, user_comment
MEMBERSHIP_GIFTING_EVENTyoutube:gift_membershipgift_memberships_count, gift_memberships_level_name
GIFT_MEMBERSHIP_RECEIVED_EVENTyoutube:gift_membership_receivedmember_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

EventLumio event typeKey fields
POLL_EVENTyoutube:poll / youtube:poll_endquestion_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.

OperationEndpointToken source
Send messagePOST /youtube/v3/liveChat/messagesget_provider_token("google")
Ban / timeoutPOST /youtube/v3/liveChat/bansget_provider_token("google")
Delete messageDELETE /youtube/v3/liveChat/messages?id=\{message_id\}get_provider_token("google")

All OAuth tokens are managed by the centralized Token Refresh Worker.

Quota Impact

OperationUnitsFrequency
InnerTube chat polling0Per poll interval (~3 s per active stream)
InnerTube broadcast discovery0Every 60 s
InnerTube statistics (likes, views)0Every 60 s per broadcast
liveChatId resolution (Data API)5Once per broadcast, cached forever
REST broadcast discovery fallback1Every 60 s (only when InnerTube fails + rest_fallback_enabled)
Chat sending200Per message sent
Moderation action200Per action
Channel enrichment1On demand, cached

Key Files

FilePurpose
crates/lo-youtube-api/src/innertube/mod.rsInnerTube get_live_chat polling + API key/continuation bootstrap
crates/lo-youtube-api/src/innertube/parser.rsParse get_live_chat responses into InnerTubeChatEvent variants
crates/lo-youtube-api/src/innertube/events.rsInnerTubeChatEvent enum definition
crates/lo-youtube-api/src/innertube/browse.rsInnerTube browse (broadcast discovery, viewer counts, likes)
crates/lo-youtube-api/src/innertube/scraper.rsScrape API key + client version from youtube.com
crates/lo-youtube-api/src/streaming.rsgRPC streamList client (fallback)
crates/lo-youtube-api/src/client.rsREST Data API v3 client (fallback + moderation)
apps/api/src/workers/youtube.rsMulti-stream worker: broadcast discovery, per-stream task lifecycle, InnerTube chat loop
apps/api/src/graphql/youtube.rsyoutubeActiveStreams GraphQL query
apps/api/src/routes/youtube_streams.rsGET /v1/youtube/active-streams REST endpoint
apps/api/src/graphql/channel_status.rschannelStatus query (includes broadcastStatus, likeCount, totalViews, scheduledStart, liveChatId)
apps/web/src/hooks/use-youtube-streams.tsFrontend hook polling active streams
apps/web/src/app/(app)/dashboard/chat/multichat.tsxBroadcast sub-menu + reply context