Search documentation

Search pages and headings

Client Library

Usage

Construct the client, connect, stream messages, and stop cleanly.

The shortest version

import { LiveChatApiClient } from '@gettersethya/yt-livechat-client'

const client = new LiveChatApiClient({
  baseUrl: 'http://localhost:3000',
  videoUrl: 'https://www.youtube.com/watch?v=zvwJ29RFVww',
})

client.on('message', (message) => {
  console.log(`${message.author}: ${message.message}`)
})

await client.connect()
await client.start()

The client is an SSE consumer: it opens one long-lived connection and reads frames off it. It does not poll, and it never sees a continuation token — the server owns both.

Constructing

new LiveChatApiClient(
  { baseUrl: string, videoUrl: string },
  options?: LiveChatClientOptions,
)
Argument Field Description
required baseUrl Your API server's origin. Trailing slashes are trimmed
required videoUrl A YouTube URL or a bare 11-character id
optional options See Events & Options

The constructor resolves videoUrl immediately and throws an ApiHttpError with code INVALID_VIDEO_ID if it cannot. This is a synchronous throw, not a rejected promise:

import { ApiHttpError, LiveChatApiClient } from '@gettersethya/yt-livechat-client'

try {
  const client = new LiveChatApiClient({ baseUrl, videoUrl: userInput })
} catch (error) {
  if (error instanceof ApiHttpError) {
    console.error(error.code) // "INVALID_VIDEO_ID"
  }
}

Accepted video URL forms

zvwJ29RFVww
https://www.youtube.com/watch?v=zvwJ29RFVww
https://youtu.be/zvwJ29RFVww
https://www.youtube.com/shorts/zvwJ29RFVww
https://www.youtube.com/embed/zvwJ29RFVww
https://www.youtube.com/live/zvwJ29RFVww

Extra query parameters are fine. To validate input before constructing, use extractVideoId, which returns null instead of throwing:

import { extractVideoId } from '@gettersethya/yt-livechat-client'

if (extractVideoId(userInput) === null) {
  showError('That does not look like a YouTube video link.')
}

The lifecycle

connect()

Opens GET /v1/stream and reads frames up to and including init, then emits connected and resolves with the client itself. No message events are delivered yet.

Doing the work up to init here is deliberate: a bad video id or a chat that does not exist fails at connect() with a real HTTP status, rather than halfway through an already-successful response.

await client.connect()
console.log(client.videoId)        // "zvwJ29RFVww"
console.log(client.init?.chatType) // "liveChatRenderer"
console.log(client.init?.cursor)   // 4211

Calling start() before connect() throws an ApiHttpError with code INTERNAL and the message call connect() before start().

start()

Drains the stream. Messages arrive via the message event; the returned promise resolves only once the stream is over — because the chat finished, because too many errors accumulated, or because you called stop().

If the connection drops, start() reconnects on its own, sending the last cursor it saw as Last-Event-ID so the server resumes exactly where it left off. A reconnect is not an error and does not end the stream.

await client.start()
console.log('stream finished')

Because it is long-running, do not await it if you have other work to do on the same task:

void client.start()

stop()

Returns immediately and never blocks. It aborts the connection and interrupts message delivery.

setTimeout(() => client.stop(), 60_000) // stop after a minute

Queued-but-undelivered messages are dropped on stop(). When a stream ends naturally the queue is drained in full first — so stop() is a cancel, not a graceful flush.

Reading a video's chat into an array

import { LiveChatApiClient } from '@gettersethya/yt-livechat-client'
import type { MessageSchema } from '@gettersethya/yt-livechat-client'

async function collect(videoUrl: string) {
  const client = new LiveChatApiClient({ baseUrl: 'http://localhost:3000', videoUrl })
  const messages: MessageSchema[] = []

  client.on('message', (message) => messages.push(message))

  await client.connect()
  await client.start()   // resolves when the replay is exhausted

  return messages
}

This works well for a finished stream's replay, which terminates on its own. A live stream will not stop until the broadcast does, so pair it with stop().

History on connect

By default a new connection is primed with the last 50 messages the server has archived for that video, delivered as ordinary message events before the live ones. A brand-new viewer therefore lands mid-conversation rather than staring at an empty pane.

These are real messages, not a replay: they carry the same cursors, and reconnecting will not deliver them twice.

Reconnecting to the same video

You do not need to. A dropped connection is repaired internally, from the cursor, without a gap.

Create a new client only when you want to follow a different video. An instance binds to one stream for its lifetime.

Rendering in a UI

message.message is plain text and fine for logs. For a chat UI, render from message.parts so custom emoji appear as images — see Rendering Message Bodies. If you are using React, Vue or Svelte, Framework Bindings gives you a useLiveChat hook that handles the whole lifecycle above for you.