Audience rooms
A relay room can hold two populations at once. Participants are the players: they are in presence, they see each other join and leave, and they talk to the room. Audience peers are viewers: they send small inputs in, receive almost nothing back, and never appear in presence.
Use an audience for stream votes, cheers, or other small inputs from viewers.
Turn it on
Set audienceRole to enable an audience. Without it, all peers are participants.
Name it in irtio.json and deploy:
{
"project": "p_...",
"name": "Quiz Night",
"audienceRole": "audience"
} A viewer is anyone the platform seats in that role, and only an audience credential can do it: a
client that simply names the role in its own join is refused with E_AUTH. audience is the
conventional name and the one joinRelay({ audience: true }) gets back when you pick it, but the word is yours: if your game
already calls a seat audience, choose something else and those players keep their seats. A role
name is 1 to 32 characters of letters, digits, _, -, . or :.
Removing the key leaves the setting alone. To turn the audience off again, set it to null and
deploy, or PATCH /v1/projects/<id> with { "audienceRole": null }.
Join as a viewer
// viewer.ts
import { joinRelay } from '@irtio/client';
const room = await joinRelay({
room: 'ABCD', // the code shown on the stream
audience: true, // mint a viewer credential and join with the audience role
});
// Fan-in: address the host, never the whole room.
room.message({ role: 'host' }, encodeVote('left'));
// What the host sends to the audience role arrives here.
room.onMessage((from, bytes) => showPrompt(decode(bytes))); audience: true does two things: it fetches a short-lived credential for that room and it joins
with the role the credential names. Pass role as well and joinRelay throws, because the
credential already decided which seat it bought.
There is no account, no sign-in, and nothing written to the viewer’s browser. Closing the tab leaves nothing behind.
Read the crowd from the game
// host.ts
import { joinRelay } from '@irtio/client';
const room = await joinRelay({ room: 'ABCD', role: 'host' });
room.audienceCount; // viewers, as of the last update
room.on('audience', (n) => drawCount(n)); // fires when that number moves
// Send a question to viewers.
room.message({ role: 'audience' }, encodePrompt('left or right?'));
// Talk to the players. Viewers do not receive this.
room.message('all', encodeTick(state)); audienceCount updates on a cadence of about two seconds rather than on every join, so a crowd
arriving all at once costs the host one message every couple of seconds instead of one per viewer.
Audience behavior
| Rule | What it means for you |
|---|---|
| Viewers are not in presence | room.clients never contains a viewer, on any client. A viewer’s own clients is empty. Use audienceCount for the size of the crowd |
| A viewer’s messages must be addressed | { role: ... } or a client id. 'all' is refused by name with E_AUDIENCE_SEND and the connection stays open |
| A viewer cannot send chat | Refused the same way. Chat is a participant surface in both directions |
'all' does not reach viewers | A room-wide message goes to participants only. To reach the crowd, address their role |
| Viewers have their own cap | 256 seats, 64 on the free plan, counted separately from the 64 player seats. A full audience is refused with E_AUDIENCE_FULL |
| Viewers have their own input budget | 2 messages a second, burst 10. Over that, the message is dropped, the sender is told once, and the socket stays open |
Remove a viewer
The room’s creator can eject a peer from the room. The creator is the first participant to join the empty room, and it needs an identity to eject:
// host.ts
const room = await joinRelay({ room: 'ABCD', role: 'host', identity: true });
room.eject(clientId, 'spamming the vote'); They are disconnected with E_EJECTED and their credential subject is kept out of the room until
the room ends. Other participants and viewers cannot eject, and nobody can eject the creator. Moderation has the rest of the ladder, including durable
project-wide bans and patterns for crowd games that hold up against brigading.
Designing around the delay
Use timed vote windows and a visible countdown to account for broadcast delay. Measure the delay on your stream before choosing a window length.
Twitch games has that pattern written out, along with the second way a viewer can send input: a chat command, from someone who never opens a page at all.
Credentials
A viewer’s credential is minted by POST /v1/audience with a project and a room, and nothing else.
It is signed by the platform, scoped to that project and that room, and expires in three hours.
- It works only for the room it was minted for. A credential read off one stream is worth nothing pointed at another room in the same project.
- It creates no account and consumes none of your project’s identity mint allowance.
- It is not an identity. It does not carry a player id you can key storage on, and it cannot be used to take a participant seat.
- Expiry is the only revocation. Closing a room does not invalidate the credentials already handed out, and a viewer rejoining a reopened room of the same name inside the window takes an audience seat they could have taken anyway.
- Every audience join requires a credential. Use
audience: trueto mint one; naming the audience role with a project key alone is refused withE_AUTH.
Both mint limits are in limits.
Errors you will see
| Code | When | Fatal |
|---|---|---|
E_AUDIENCE_FULL | The audience cap is reached | Yes, the join is refused |
E_AUDIENCE_SEND | A viewer addressed the whole room, sent chat, or went over the input budget, or a peer without the right to eject sent one | No |
E_TOKEN_WRONG_PROJECT | The credential names another project or another room | Yes |
E_TOKEN_EXPIRED | The credential is older than three hours | Yes, mint another |
E_EJECTED | The room’s creator removed this peer, or it was ejected earlier from this room | Yes |