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

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

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. Use audience: true.

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 budgetNo
E_TOKEN_WRONG_PROJECTThe credential names another project or another roomYes
E_TOKEN_EXPIREDThe credential is older than three hoursYes, mint another
E_EJECTEDA participant removed this peer, or it was ejected earlier from this roomYes