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.
This is the shape a streamed game wants. A few people play, a link goes out on the broadcast, and several hundred strangers arrive in the next ten seconds to vote, cheer, or spawn something. Every one of them costs the room a socket and a trickle of inbound messages, and nothing else.
Turn it on
An audience exists once your project names the role that means “viewer”. It is off until then, and nothing changes for a project that names nothing: every peer is a participant, exactly as before.
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
// Ask the crowd a question. This is the O(N) send, and it is deliberate.
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.
The rules, and why each one is there
| 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 |
The two send rules are the same rule from both ends: the room’s default fan-out stays proportional to the number of players, however many thousand people are watching, and reaching the crowd is something you write down on purpose.
Dropping over-budget input rather than closing the connection is deliberate too. A vote that did not count is a normal outcome in a vote window; a viewer knocked off the stream for tapping quickly is not.
Remove a viewer
A participant can eject a peer from the room:
// host.ts
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. In a room with an audience configured, any participant may send this and a viewer
may not. 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
A viewer watching a broadcast is two to four seconds behind the game. Build for it rather than against it: open a vote window, show a countdown on screen, and resolve when it closes. Every successful audience-participation game works this way. Do not ask viewers to react to a frame.
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.
- A credential is the only door to the audience. This used to be untrue, and the guide used to say
so: a client that joined with your project key and named the audience role took an audience seat
with no credential at all. The reasoning was that the role only ever restricts, so claiming it
buys a client less than the participant seat it could have asked for. Two things were wrong with
that. The audience cap is much larger than a small room’s participant cap, so a claimed seat is
the cheaper one to take a lot of. And a claimed seat carries no subject, which is what ejects and
ban lists key on, so the one population you may need to moderate was the one that could arrive
unidentified. Both hosts now refuse a join that names the audience role without a credential, and
the error says to mint one at
POST /v1/audience. Useaudience: true.
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 | 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 | A participant removed this peer, or it was ejected earlier from this room | Yes |