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

RuleWhat it means for you
Viewers are not in presenceroom.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 chatRefused the same way. Chat is a participant surface in both directions
'all' does not reach viewersA room-wide message goes to participants only. To reach the crowd, address their role
Viewers have their own cap256 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 budget2 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: true to mint one; naming the audience role with a project key alone is refused with E_AUTH.

Both mint limits are in limits.

Errors you will see

CodeWhenFatal
E_AUDIENCE_FULLThe audience cap is reachedYes, the join is refused
E_AUDIENCE_SENDA viewer addressed the whole room, sent chat, or went over the input budget, or a peer without the right to eject sent oneNo
E_TOKEN_WRONG_PROJECTThe credential names another project or another roomYes
E_TOKEN_EXPIREDThe credential is older than three hoursYes, mint another
E_EJECTEDThe room’s creator removed this peer, or it was ejected earlier from this roomYes