Appearance
A group room
A room joined by name or by link: a team's standing meeting, a class, a consultation several people attend. Up to 100 people, each sending their media once to Talkline's media server, which forwards it to everyone else.
Make the room
There are two ways, and the difference is who decides that a room exists.
Your backend creates it, and hands out its sid. Use this when the room is a thing in your product — a scheduled meeting, a case:
ts
const room = await talkline.rooms.create({
name: 'case-4821', // optional; unique among your live rooms
maxParticipants: 12,
emptyTimeoutSec: 600, // keep it ten minutes before anyone arrives, and after the last one leaves
metadata: { caseId: 4821 },
});
// room.sid — RM_… — goes into the link you shareThe first person to join creates it, when their token carries roomCreate. Use this for a room that is just a name — standup-team-7 — and should exist whenever someone is in it.
Once it exists, a room made by a join is like any other. room.sid is set when join() resolves, so a link can carry it, and a token scoped to that sid — room: sid, without roomCreate — joins it while it lasts and is refused room_closed once it has ended, rather than starting a new room. A room made by a join holds up to 50 people; create it from your backend to choose up to 100 (maxParticipants).
A room ends once it has been empty for its empty timeout — 60 seconds unless you set one — and its name is then free for a new room. DELETE /v1/rooms/{sid} ends it at once.
Tokens for a room
Scope each person's token to the room with the room grant, so the link to one meeting cannot be used to enter another:
ts
// Someone with a link to the room your backend created:
await talkline.mintToken({
identity: `app:user:${user.id}`,
name: user.displayName,
grants: {
roomJoin: true,
room: room.sid,
canPublish: true,
canSubscribe: true,
canPublishData: true,
canPublishSources: ['microphone', 'camera', 'screen'],
},
});
// A standing room created by whoever arrives first:
await talkline.mintToken({
identity: `app:user:${user.id}`,
grants: { roomJoin: true, roomCreate: true, room: 'standup-team-7', canPublish: true, canSubscribe: true },
});Give the host roomAdmin as well, to mute and remove others and end the room. Someone outside your product — a guest with no account — gets an identity like guest: plus a random UUID, and the same kind of token.
Join
ts
const room = await client.join({ room: 'RM_4f1c2a9e0b7d3c5a8e61' });
await client.localParticipant.enableMicrophone();
await client.localParticipant.enableCamera();join({ room }) takes a sid or a name; leave room out and it joins the room the token is scoped to. Turn media on after joining: the room says what it is first, and each track is then set up for it.
A client is in one room at a time. join(), call() and answering a call are refused invalid_state while it is in one: leave it first with room.leave(), and the next join waits for the leave by itself.
Everyone already in the room is room.participants. Their tracks arrive as trackSubscribed, and people arriving and leaving as participantJoined and participantLeft:
ts
client.on('participantJoined', (p) => addTile(p.identity, p.displayName));
client.on('participantLeft', ({ identity }) => removeTile(identity));
client.on('trackSubscribed', (track, participant) => {
if (participant) tileFor(participant.identity).show(track);
});
client.on('activeSpeakers', (identities) => highlight(identities));A participant can send a camera and a screen at once, so pick tracks by source — 'camera', 'screen' — rather than taking the first video track.
Bandwidth: say what is on screen
The media server can send each person a smaller version of a video, or none at all, but only if it knows how each video is shown. Tell it, per remote video track:
ts
track.setDisplay({ visible: true, width: 320, height: 180 }); // a small tile
track.setDisplay({ visible: false }); // scrolled away or hiddenIn React, useVisibleTile(track) does this for you from the element's size and visibility (React). A grid of thirty people where only the visible tiles are sent at the size they are shown is what keeps a large room working on an ordinary connection.
When a person's connection stays too slow for video even so, the media server sends them audio only and pauses the video, rather than let it freeze. room.audioOnlyFallback says so, and the audioOnlyFallback event fires with true when it starts and false when video is back; the server tries video again on its own. Tell the person in your own words — a frozen tile with no explanation reads as a broken call.
ts
client.on('audioOnlyFallback', (active) => banner.toggle(active, 'Your connection is slow: audio only for now'));Data messages
Small messages to everyone in the room, or to some of them — reactions, a raised hand, a cursor:
ts
await room.sendData({ type: 'raise-hand' }, { topic: 'reactions' });
await room.sendData('ping', { to: ['app:user:9'], reliable: false });
client.on('dataReceived', (message) => {
if (message.topic === 'reactions') showReaction(message.from, message.json());
});A message is up to 15 KB. Reliable messages (the default) arrive in order and survive a reconnect; unreliable ones may not, and are right for anything the next message replaces. A message whose from is empty was sent by your backend, with POST /v1/rooms/{sid}/data. Talkline carries data messages and never stores them: chat history belongs in your own system.
Leave
ts
await room.leave();From your backend
| To | Call |
|---|---|
| See who is in a room | GET /v1/rooms/{sid} — talkline.rooms.get(sid) |
| Mute someone's track, or change their grants | PATCH /v1/rooms/{sid}/participants/{identity} — talkline.rooms.updateParticipant(sid, identity, body) |
| Remove someone | DELETE /v1/rooms/{sid}/participants/{identity} — talkline.rooms.evict(sid, identity) |
| Send everyone a message | POST /v1/rooms/{sid}/data — talkline.rooms.sendData(sid, { payload }) |
| End the room | DELETE /v1/rooms/{sid} — talkline.rooms.end(sid) |