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/zvwJ29RFVwwExtra 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) // 4211Calling 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 minuteQueued-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.