Signaling
Every channel carries messages and presence alongside the audio and video. That is what a live comment feed, a viewer count and an audience list are built from — and it needs no second connection, no second server, and no extra latency, because it travels the same path the media does.
Note
Messages#
sendMessage broadcasts to everyone in the channel. There is no addressing — everyone receives everything, which is what makes it right for comments and wrong for private replies.
An audience member can send messages even though they cannot publish video. That asymmetry is the point: a viewer of a broadcast can comment without ever being able to appear on stage.
// A commentawait engine.sendMessage('First!');// Anything structured — a reaction, a poll vote, a raised handawait engine.sendMessage({ kind: 'reaction', emoji: '🎉' });engine.on('message', (m) => {console.log(m.from?.displayName, m.text ?? m.data);});
Note
sendMessage resolves — it returns the message it sent, id and all. The useMessages hook does this for you.What a message carries
| Field | Type | Notes |
|---|---|---|
id | string | Unique to the message. Use it as a render key and to de-duplicate. |
text | string? | Set when you sent a string. |
data | object? | Set when you sent an object. |
from | AppRemoteUser? | Who sent it. Undefined if it came from your own server. |
sentAt | Date | The sender's clock. Do not order a feed by it alone — clocks disagree. |
Reliable and lossy
Messages are sent reliably by default: they arrive, in order, or the send fails. For things that are worthless a moment later — a cursor position, a live reaction burst — pass { reliable: false } and they will be dropped rather than retried under packet loss.
await engine.sendMessage({ x, y }, { reliable: false });
Presence and the audience#
Everyone in the channel is visible, whether they publish or not. Roles come from the token your server minted, so the client cannot lie about them.
engine.audienceCount; // how many are watchingengine.audience; // the viewersengine.hosts; // whoever is on stageengine.on('audience-changed', (audience) => {setViewers(audience.length);});
Note
Reading a participant
| Property | Notes |
|---|---|
role | host | cohost | audience, from the token. See Roles and permissions. |
isPublisher | False for audience. Useful for deciding what to lay out on stage. |
attributes | The metadata your server put in the token — display name, avatar, tier. Our own fields are stripped out. |
isSpeaking | Live, and updated as they speak. |
Moving between stage and audience#
Promotion is a decision your server makes — a client cannot promote itself.
- Your app asks your server to promote a viewer.
- Your server checks its own rules and calls
POST /v1/rooms/:roomName/participants/:identity/rolewith{ "role": "cohost" }. The viewer stays connected; no rejoin. - Everyone else sees the audience change.
Issuing a new token and rejoining also works. Both are described in Roles and permissions.
When you want the raw bytes#
sendData is still there for anything that needs its own wire format — a binary protocol, a compressed payload, a codec of your own. Platform frames never appear on data-received, so your bytes and our messages cannot be confused for one another.
See Data messages for that lower level.
Limits worth knowing#
- A message is capped at roughly 15 KB. Send a URL, not a file.
- Nothing is retained — no history, no delivery to people who are offline.
- Messages do not count against your minute quota; only session time does.
- There is no server-side moderation. Filter on your own backend before broadcasting anything user-generated.