Replays

A replay is a clip of your room: a few seconds of recorded state that your own game code plays back. The room keeps the recent past in memory, room.replay.clip(seconds) saves it, and the client plays it through the same pipeline it uses live, so your game draws it with your own code.

Use it for a killcam, a “clip that” button, a shareable highlight, or a bug report a player can send you.

Record and clip

Two lines. One arms the recording, one takes the clip.

// irtio/room.ts
export default defineRoom(schema, {
  tickRate: 20,
  replay: {},                       // record the last 60 seconds
  rpc: {
    clipThat(state, _params, ctx) {
      // Ask, then carry on: the handler is synchronous and the answer arrives later.
      void ctx.room.replay.clip(20).then((clip) => {   // the last 20 seconds
        state.clips.add(clip.id, { by: ctx.clientId });
      });
    },
  },
  tick(state, dt, room) { /* ... */ },
});

clip() resolves with { id }. The id is the clip: anyone holding it can watch, and there is no other permission. Hand it to the players you meant to and no further.

Nothing is stored until you call clip(). Recording costs memory in the room and no storage at all.

Play a clip back

The share link is your own game’s page with ?replay=<id> on it. Read the id from the URL and join with the replay transport instead of a live room.

// main.ts
import { joinRoom, replay, replayIdFromLocation } from '@irtio/client';
import { schema } from './irtio/schema';

const clipId = replayIdFromLocation();
const room = clipId
  ? await joinRoom(schema, {
      transport: replay(clipId, { project: 'your-project-id' }),
      room: '',                     // a replay joins no room
    })
  : await joinRoom(schema);         // the live game

// Draw exactly as you always do: room.state, room.render, onAdd, onRemove.

replay() returns the transport and the playback controls together:

CallWhat it does
seek(seconds)Jumps to seconds from the start of the clip
speed(rate)Playback rate. 0.25 for slow motion, 4 for fast forward
pause() / resume()Stops and restarts the feed
durationSeconds of footage in the clip
positionWhere playback is, in seconds
endedtrue once the last frame has played
onEnd(cb)Fires when the last frame has played

A replay session writes nothing and sends nothing. Writes, RPCs and messages your game makes during playback go nowhere, so a replay page needs no special case in your input code.

What a clip contains

Everything the recorder can see, and by default the recorder sees everything: every role’s collections, and every instance of a collection restricted by position.

That is the wrong default for a game with hidden information, because a clip of a hidden-hand game would show every hand. Name a role and the clip contains exactly what that role can see:

replay: { seconds: 30, view: 'guesser' },

The recorder has no position of its own, so a collection restricted by position is still recorded whole. If your hidden information is positional, record for a role whose visibility already excludes it, or do not record that room type.

defineRoom refuses a view your schema does not declare as a role.

Options

OptionDefaultRangeWhat it does
replayabsent{} or the fields belowAbsent means no recording at all
seconds601 to 300Seconds of footage held in memory
viewomitteda role your schema declaresRecords that role’s view instead of everything

Limits

LimitValueWhen you reach it
Recorded footageseconds, 60 by default, 300 at mostThe oldest seconds are dropped
Memory per recording room8 MBThe oldest seconds are dropped early, so a busy room holds fewer than seconds
One recorded momentmust fit in that 8 MBA room whose whole state is larger than the ring cannot record at all: recording stops, a line says so in the room’s logs, and room.replay.clip() is refused with E_CLIP_DISARMED
Seek resolution2 secondsseek() lands on the nearest recorded point at or before your target
Clips per roomone per secondA second call inside the second is refused with E_CLIP_COOLDOWN
Clip size16 MBThe clip is refused with E_CLIP_TOO_LARGE
How long a clip is kept30 daysThe clip is deleted and its link stops working

Clips count as stored bytes and as bandwidth when they are watched, the same as everything else you store. See limits.

When it goes wrong

The clip refuses to play after a deploy. A clip is pinned to the schema it was recorded on. A deploy that changes your schema changes that pin, and playback refuses rather than decoding old bytes against a new shape:

E_SCHEMA_MISMATCH: E_REPLAY_SCHEMA_MISMATCH: this clip was recorded against schema
a1b2c3d4e5f60718 and this page joined with 0918273645afbeef.

There is no migration for a clip. Treat clips as short-lived, which the 30-day window already assumes.

The link opens on someone else’s page. The blob is readable only from an origin your project has registered, plus localhost. Register the origin your game is served from in the dashboard, the same list a browser needs to open a room at all.

clip() rejects with E_CLIP_EMPTY. The room has not changed anything since it started recording, so there is nothing to clip yet.

Relay rooms cannot record. A relay room has no room file, and replay is a room-file setting.

Next steps