DEV Community

Cover image for [How to Build an AITuber] How to Fetch Viewer Comments from YouTube and Twitch
Yuki Shindo
Yuki Shindo

Posted on

[How to Build an AITuber] How to Fetch Viewer Comments from YouTube and Twitch

Hello! I’m the developer of AITuber OnAir, a web app for running AI-powered VTuber streams:

AITuber OnAir

We’ve just added a brand-new feature: Twitch chat integration. From now on, AITuber OnAir supports both YouTube Live and Twitch.

In this post I’ll share what I learned while building it—how to fetch comments from Twitch (with EventSub WebSockets) and, for completeness, how to fetch comments from YouTube Live as well.
I hope fellow AITuber developers find it useful!

1. Fetching YouTube Live chat messages

Overall flow

When you want to pull live chat from a YouTube stream, the request sequence looks like this:

[Live Video ID]
      │ videos?part=liveStreamingDetails
      ▼
[activeLiveChatId]
      │ liveChat/messages?part=snippet,authorDetails → nextPageToken
      ▼
[Chat Messages]
Enter fullscreen mode Exit fullscreen mode

You hit two endpoints:

  • Live Video ID – grab it from the URL’s v= parameter, etc.
  • activeLiveChatId – returned inside the liveStreamingDetails object of the videos endpoint.
  • liveChat/messages – loop with nextPageToken to paginate.¹ ²

Getting your YouTube API key & Live ID

You need both before you can fetch comments. I explain how to obtain them in this note (Japanese):

Enter the required settings for YouTube Live

Sample code (YouTube Live)

// youtube-chat.ts
import 'dotenv/config';
import fetch from 'node-fetch';

const API_KEY  = process.env.YT_API_KEY!;   // Data API v3 key
const VIDEO_ID = process.env.YT_VIDEO_ID!;  // Live video ID

/** Return activeLiveChatId if the stream is live and chat is enabled */
async function getLiveChatId(videoId: string) {
  const url =
    `https://youtube.googleapis.com/youtube/v3/videos` +
    `?part=liveStreamingDetails&id=${videoId}&key=${API_KEY}`;
  const res  = await fetch(url);
  const json = await res.json();
  return json.items?.[0]?.liveStreamingDetails?.activeLiveChatId ?? null;
}

/** Poll chat forever and dump to console */
async function pollChat(liveChatId: string, pageToken = '') {
  const url =
    `https://youtube.googleapis.com/youtube/v3/liveChat/messages` +
    `?liveChatId=${liveChatId}&part=snippet,authorDetails&key=${API_KEY}` +
    (pageToken ? `&pageToken=${pageToken}` : '');

  const res  = await fetch(url);
  const json = await res.json();

  // Pretty-print messages
  for (const m of json.items ?? []) {
    const author = m.authorDetails.displayName;
    const text   = m.snippet.textMessageDetails?.messageText ?? '';
    if (text) console.log(`[YT] ${author}: ${text}`);
  }

  // Wait 1–2 s and recurse (API guideline: ≥ 1 s)
  const waitMs = json.pollingIntervalMillis ?? 1000;
  await new Promise(r => setTimeout(r, waitMs));
  await pollChat(liveChatId, json.nextPageToken);
}

(async () => {
  const id = await getLiveChatId(VIDEO_ID);
  if (!id) { console.error('Stream not found or chat disabled'); return; }
  await pollChat(id);
})();
Enter fullscreen mode Exit fullscreen mode

Run it

export YT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
export YT_VIDEO_ID=YOUR_LIVE_VIDEO_ID
node youtube-chat.ts
Enter fullscreen mode Exit fullscreen mode

2. Fetching Twitch chat (EventSub WebSocket)

IRC is passé—use EventSub instead

The first docs you’ll find often describe using IRC, but as of 2024 Twitch recommends EventSub’s channel.chat.message for reading chat.

Add the user:read:chat (formerly chat:read) scope to your OAuth token and you can listen over a single WebSocket. Just remember to reconnect yourself when you receive a session_reconnect event.³ ⁴

https://x.com/shinshin86/status/1924750262528573898

Overall flow (Twitch)

[User Access Token]  +  [Channel Login]
           │ 1) GET /users?login=<channel>       (Helix Users API)
           ▼
[broadcaster_user_id]
           │ 2) GET /oauth2/validate            (Validate API)
           ▼
[user_id (token owner)]
           │ 3) Connect wss://eventsub.wss.twitch.tv/ws
           ▼    (session_welcome)
[session_id]
           │ 4) POST /eventsub/subscriptions
           │      type=channel.chat.message
           ▼
[WebSocket PUSH] (notification)
           │
[Chat Messages]
Enter fullscreen mode Exit fullscreen mode

References:
Helix Users APIValidating Tokens

Getting your token & client ID

Register an app in the Twitch Developer Console to obtain a Client ID. Detailed steps (Japanese):

Setup instructions for streaming on Twitch with AITuber OnAir

Use Public client type

AITuber OnAir is a browser-only SPA, so there’s no safe place for a client secret. Twitch recommends treating pure client-side JavaScript apps as Public and using the Implicit Grant Flow or Device Code Flow.³

  • Public client highlights

    • No client secret—one is never generated.
    • You don’t send a secret when getting or refreshing tokens.
    • The Client ID itself is public, so including it in HTML/JS is fine.⁵ (In production we still let users provide their own ID instead.)
    • Only Implicit or Device-Code flows are available (Client-Credentials etc. won’t work).⁶

Twitch’s own chart (“Getting OAuth Access Tokens”) compares the four flows suitable for SPAs—worth bookmarking when you decide which to use.

Sample code (Twitch)

import 'dotenv/config';
import fetch from 'node-fetch';
import WebSocket from 'ws';

const TOKEN     = process.env.TWITCH_TOKEN!;     // user access (user:read:chat)
const CLIENT_ID = process.env.TWITCH_CLIENT_ID!; // app’s Client ID
const CHANNEL   = process.env.TWITCH_CHANNEL!;   // broadcaster’s login name

(async () => {
  /* 1) Get broadcaster_user_id */
  const uRes = await fetch(
    `https://api.twitch.tv/helix/users?login=${encodeURIComponent(CHANNEL)}`,
    { headers: { Authorization: `Bearer ${TOKEN}`, 'Client-Id': CLIENT_ID } }
  );
  const broadcasterId = (await uRes.json()).data?.[0]?.id;
  if (!broadcasterId) throw new Error('invalid channel');

  /* 2) Get own user_id (token owner) */
  const vRes = await fetch('https://id.twitch.tv/oauth2/validate', {
    headers: { Authorization: `OAuth ${TOKEN}` }
  });
  const userId = (await vRes.json()).user_id;

  /* 3) Connect WebSocket */
  const ws = new WebSocket('wss://eventsub.wss.twitch.tv/ws');

  ws.on('open', () => console.log('🟢 connected'));
  ws.on('close', () => console.log('🔴 closed'));

  ws.on('message', async raw => {
    const msg  = JSON.parse(raw.toString());
    const type = msg.metadata?.message_type;

    if (type === 'session_welcome') {
      // 4) Subscribe immediately
      const sessionId = msg.payload.session.id;
      await fetch('https://api.twitch.tv/helix/eventsub/subscriptions', {
        method : 'POST',
        headers: {
          Authorization : `Bearer ${TOKEN}`,
          'Client-Id'   : CLIENT_ID,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          type    : 'channel.chat.message',
          version : '1',
          condition : { broadcaster_user_id: broadcasterId, user_id: userId },
          transport: { method: 'websocket', session_id: sessionId }
        })
      });
      return;
    }

    if (type === 'notification'
        && msg.payload.subscription.type === 'channel.chat.message') {
      const txt = msg.payload.event.message.text;
      const usr = msg.payload.event.chatter_user_name;
      console.log(`[TW] ${usr}: ${txt}`);
    }

    if (type === 'session_reconnect') {
      // 5) Switch to the new URL
      ws.removeAllListeners();          // drop old listeners
      const newWs = new WebSocket(msg.payload.session.reconnect_url);
      newWs.on('message', ws.listeners('message')[0]!);
    }
  });
})();
Enter fullscreen mode Exit fullscreen mode

Run it

export TWITCH_TOKEN=oauth_xxxxxxxxxxxxxxxxxxxx
export TWITCH_CLIENT_ID=yyyyyyyyyyyyyyyyyyyyy
export TWITCH_CHANNEL=mychannel
node twitch-chat.ts
Enter fullscreen mode Exit fullscreen mode

Wrap-up

  • YouTube: two API calls (videosliveChat/messages) + periodic polling.
  • Twitch: a single EventSub WebSocket—subscribe to channel.chat.message and receive pushes.
  • Both are under 100 lines of script for real-time chat capture.

From here you can add buffering, banned-word filters, AI summarization, and more to build your own custom AITuber workflow.

Hope this helps—happy streaming!


¹ LiveChatMessages: list | YouTube Live Streaming API
² LiveChatMessages | YouTube Live Streaming API
³ EventSub – Twitch Developers
Handling WebSocket Events – Twitch Developers
Registering Your App
Getting OAuth Access Tokens

Top comments (0)