Appearance
The web SDK
@talkline/core is the whole client: the connection to the signaling server, rooms and calls, the WebRTC media, and recovery from network trouble. It has no UI and no framework. @talkline/react adds hooks over it for React apps (React).
Install
@talkline/core, with @talkline/protocol, which it depends on, and @talkline/react in a React app, come from the tarballs of a Talkline release, which your app keeps in its own repository: Installing the SDKs.
@talkline/core runs in any current browser with WebRTC. Camera and microphone access needs a secure context: serve your app over https (or from localhost while developing).
Create a client
One TalklineClient per signed-in user, created once and kept for as long as the page is open:
ts
import { TalklineClient } from '@talkline/core';
const client = new TalklineClient({
url: 'wss://signal.talkline.example',
token: async () => {
const res = await fetch('/api/talkline-token', { method: 'POST', credentials: 'include' });
if (!res.ok) throw new Error(`token endpoint answered ${res.status}`);
return (await res.json()).token;
},
});url is your deployment's signaling address; the SDK adds the path and protocol version it needs.
The token is a function
token is a provider: a function the SDK calls whenever it needs a token — for the first connect, and again for every reconnect. Tokens are single-use, and a client that reconnects with the token it first connected with is refused token_replayed at exactly the moment it cannot ask the user for anything.
The provider is passed { reason: 'initial' | 'reconnect', identity }, for logging. It may return a string or a promise of one. A plain string also works, for a test or a one-shot page — it simply cannot survive a reconnect, and the SDK raises token_required rather than looping.
When the provider throws or rejects:
- On the first connect,
connect()— or thecall()orjoin()that connected — rejects with exactly what your provider threw, not wrapped, and the client isclosed. Catch it there: a signed-out user is your app's to handle. - On a reconnect, the attempt counts as failed and is tried again with backoff, calling your provider again each time, until the session's 15 minutes are spent and the client gives up with
session_expired. A provider that will never succeed again — the user signed out — should be answered withclient.close(), which stops the retries.
Connect
ts
await client.connect();connect() resolves once the server has accepted the token. It is safe to call more than once — a second call joins the first — and call() and join() connect on their own if needed. A client that should be reachable for incoming calls connects as soon as the user is signed in, and stays connected.
After connecting, client.grants holds what the server actually granted, which can be narrower than the token asked for. Draw your controls from it: no camera button when canPublishSources leaves out camera.
Events
The client is an event emitter. on returns a function that removes the listener:
ts
const off = client.on('incomingCall', (call) => showRinger(call));
// later
off();| Event | Arguments | When |
|---|---|---|
connectionState | state, { cause, attempt } | The connection changed state — Connection and recovery |
incomingCall | call | Someone is calling this identity |
callUpdated | call | Anything about the current call changed — read it again |
callStarted | call | The call was answered and is up |
callEnded | { reason, call } | The call ended, however it ended — once per call. reason is from this side: hangup when this client hung up, remote_hangup when the other side did; call.endReason keeps it |
roomJoined | room | This client took a seat — by join(), by placing a call, or by answering one |
roomLeft | { reason } | This client left its room, or was removed |
participantJoined / participantLeft / participantUpdated | participant / { identity } / participant | Someone else arrived, left or changed |
trackSubscribed / trackUnsubscribed | track, participant | Someone else's audio or video arrived or went away |
localTracksChanged | tracks | This client's own tracks changed |
activeSpeakers | identities | Who is speaking now, loudest first |
recordingChanged | recording | The room's recording started, stopped or failed, or this user acknowledged it; also on joining a room being recorded. Show it — Recording a room |
dataReceived | message | A data message arrived |
quality | level, identity | A connection quality grade: this client's own, or, for a room admin, someone else's |
audioOnlyFallback | active | The media server sends this client audio only, its connection too slow for video (true), or video is back (false); room.audioOnlyFallback keeps it — A group room |
stats | samples | This client's own media numbers, every 5 seconds while in a room — for a diagnostics panel |
mediaRecovery | { step, of } | Media stalled and the SDK is recovering it, step by step — Connection and recovery |
presence | info | A watched identity's presence changed |
needsUserGesture | track | The browser blocked audio playback until the user interacts — show a button that calls client.resumeAudio() |
updateDue | protocol | This build of the SDK is getting old for the deployment; ship an update |
disconnected | { reason, reconnectAllowed } | The server ended the session |
error | error | Something failed that no promise was waiting for |
Every event and its arguments are in the ClientEvents reference.
Showing media
A Track is one stream of audio or video. Attach it to an element:
ts
client.on('trackSubscribed', (track, participant) => {
const el = track.attach(); // creates an <audio> or <video> element, or pass your own
document.querySelector('#remote')!.append(el);
});
client.on('trackUnsubscribed', (track) => track.detach());Use separate elements for a participant's audio and video, so that hiding or muting a video element can never silence the call. Your own camera is in client.localParticipant.tracks; show it in a muted video element.
The words are yours
The SDK ships no user-facing text — not a word, in any language. It emits states and error codes, and your app chooses what to say about each: "Reconnecting…", "Your camera is blocked", "They declined". Branch on error.code, never on error.message. Handling errors.
Closing
ts
await client.close();when the user signs out. It leaves the room it is in — which hangs up a one-to-one call — and closes the connection. Create a new client for the next user rather than reusing a closed one.