Player SDK
Player SDK
@antcdn/live-signaling gives you the live room without any interface. You get events and you draw whatever you like.
It works with any player. It does not depend on the AntCDN player, and it never touches the DOM.
npm install @antcdn/live-signalingConnect
import { connectLiveRoom } from '@antcdn/live-signaling';
const room = connectLiveRoom({ streamId: 'ls_9f2a' });connectLiveRoom opens the connection straight away and returns a LiveRoom.
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
streamId | string | required | The live stream to join. |
baseUrl | string | https://worker.antcdn.net | Override for self hosting or local development. |
sessionId | string | generated | Identifies the viewer so they cannot answer twice. |
revealImmediately | boolean | false | Show questions on arrival instead of waiting for the video. See below. |
webSocketFactory | function | browser WebSocket | Inject a socket, for tests or non browser runtimes. |
Events
Subscribe with on. It returns a function that unsubscribes.
const stop = room.on('question', (question) => showPoll(question));
// laterstop();| Event | Payload | Meaning |
|---|---|---|
welcome | { streamId, viewerCount, hasVoted } | Connected. Includes whatever state the room already had. |
question | LiveQuestion | A question, at the moment it should be shown. |
tally | LiveTally | Running results. |
closed | LiveTally | The host closed the question. These are the final numbers. |
count | { viewerCount } | Audience size. Coalesced on the server, so this is not chatty. |
error | { code, message } | The server rejected something. |
status | { connected, reconnecting } | Transport state, for a reconnecting indicator. |
Error codes
| Code | Meaning |
|---|---|
already_voted | This viewer has answered already. Common and harmless. |
no_active_question | The question is not open. It probably just closed. |
unknown_option | No option with that index. |
text_not_allowed | The question does not take written answers. |
text_too_long | Longer than 280 characters. |
bad_message | The server could not read the message. |
Telling the SDK where playback is
This is the step that makes questions appear at the right moment, and the one most easily missed.
Your player reads timed metadata out of the video. When it does, hand that to room.cue(). The SDK then reveals the question.
// Shaka PlayershakaPlayer.addEventListener('metadata', (event) => { const question = questionFromMetadata(event); if (question) room.cue(question);});The cue carries the whole question, so cue() shows it immediately. There is no round trip at the moment the question appears, and it still works if the socket has dropped.
If your encoder only gives you an id rather than the full payload, pass the id instead. The SDK then waits for the question to arrive over the socket before showing it:
room.cue('lsq_71c4a9');Caution
If you never call cue(), questions never appear. The SDK holds them on purpose. To show questions as soon as they arrive, set revealImmediately: true. Use that only for hosts and moderators, who are not watching on delay. For ordinary viewers it brings back exactly the early reveal problem cues exist to prevent.
What the cue looks like
The cue is an ID3 TXXX frame whose text is the question as JSON:
{ "id": "lsq_71c4a9", "prompt": "Which build should we ship first?", "options": ["The rewrite", "The patch"], "allowFreeText": false}Most players surface ID3 frames through a metadata event. Read the frame text, parse it, and pass the result to cue():
function questionFromMetadata(event) { const frame = event.payload ?? event; if (frame.key !== 'TXXX') return null;
let question; try { question = JSON.parse(frame.data); } catch { return null; // unrelated timed metadata in the same stream }
// Check the shape. This arrives from the media, so a malformed payload should be // dropped rather than drawn. if (typeof question?.id !== 'string' || !question.id.startsWith('lsq_')) return null; if (typeof question.prompt !== 'string' || question.prompt === '') return null; if (!Array.isArray(question.options) || question.options.length === 0) return null; if (!question.options.every((option) => typeof option === 'string')) return null;
return question;}Other streams may put their own ID3 frames in the same video. Always check the shape before showing anything, or you will draw someone else’s metadata as a question.
Tip
The hosted player already does all of this. You only need this code if you are using a different player. See Using the hosted player.
Answering
room.vote(question.id, 1); // choose option Broom.vote(question.id, 1, 'and here is why'); // choose and writeroom.vote(question.id, FREE_TEXT_ONLY, 'my answer'); // write onlyFREE_TEXT_ONLY is exported from the package and is -1. Written answers are limited to MAX_FREE_TEXT_LENGTH, which is 280 characters, and are only accepted when the question was created with allowFreeText.
A viewer answers once. A second attempt comes back as an error with code already_voted.
Types
interface LiveQuestion { id: string; prompt: string; options: string[]; allowFreeText?: boolean; cued?: boolean;}
interface LiveTally { questionId: string; counts: number[]; // lines up with options total: number; textCount?: number; // written answers, counted separately}counts and total cover the options only. textCount is reported on its own because a written answer is not a choice between the options.
Closing the connection
room.close();Call this when the viewer leaves the page or the player is torn down. The SDK reconnects on its own if the connection drops, so only call close() when you actually mean to stop.
A complete example
import { connectLiveRoom } from '@antcdn/live-signaling';// questionFromMetadata is the helper shown above.
const room = connectLiveRoom({ streamId: 'ls_9f2a' });let current = null;
room.on('question', (question) => { current = question; renderPoll(question);});
room.on('tally', (tally) => { if (current && tally.questionId === current.id) renderResults(tally);});
room.on('closed', (tally) => { renderFinalResult(tally); current = null;});
room.on('error', ({ code, message }) => { if (code === 'already_voted') return; // harmless console.warn(message);});
// Questions appear when playback reaches them.shakaPlayer.addEventListener('metadata', (event) => { const question = questionFromMetadata(event); if (question) room.cue(question);});
onAnswerClick((optionIndex) => room.vote(current.id, optionIndex));window.addEventListener('pagehide', () => room.close());Replaying over a recording
See Recordings.