# Replays and clips

> Record a room tick by tick, keep its best moments as clips, and play either back in the browser with the code that renders the live game.

A **replay** is a recording of a room's synced state, tick by tick: what
a client that sees every entity would have seen. A **clip** is the last
few seconds of a replay, kept after the replay itself expires. You play
both with `@irt/client/replay`, into an entity store with the same
events as a live room, so the code that renders your game renders its
replays too.

Nothing is recorded unless the room asks. Replays are never public: your
backend downloads them with its API key and decides who may watch.

## Recording

```ts
import { defineRoom } from "@irt/room";
import { schema } from "./schema.js";

export default defineRoom({
  schema,
  onCreate(room) {
    room.data.replay = room.record();
  },
  onWake(room) {
    room.data.replay = room.record();
  },
  onTick(room) {
    // ...
  },
});
```

`room.record()` starts recording from the room's next tick and returns
the replay's ID. Recording stops with `room.stopRecording()`, when the
room hibernates or stops, and when the replay reaches 64 MB or 2 hours.
A recording isn't room state, so a room that always records calls
`record` in `onWake` as well as `onCreate`; after a hibernation, or a
crash of its room host, it's a new replay. `room.recording` is the ID
being recorded, or undefined.

Options:

- `id`: your own ID, such as a match ID, 1 to 64 of `A-Z`, `a-z`, `0-9`,
  `_` and `-`. A room uses an ID once; recording under an ID it has used
  keeps nothing, and the project's logs say so. Without one you get a
  random ID.
- `private`: also record each player's private state. **Off by
  default.** Whoever can watch a replay sees everything in it, so turn
  this on only for replays that only you or the player see, or after a
  game where hands no longer matter.
- `messages`: also record what the room broadcasts to every connection,
  such as chat or kill feed events. Messages to one connection are never
  recorded.

```ts
room.record({ id: `match-${matchId}`, messages: true });
```

`record` throws when the room is already recording, and when it started
a recording less than 10 seconds ago.

## What a replay holds

The shared state, every entity and presence, every tick that changed
any of them, and a full state every 10 seconds so playback can seek.
Interest doesn't apply: a replay sees everything, whoever could see it
at the time. Owner writes appear as the changes they made; corrections,
inputs and client messages aren't recorded.

A replay carries the kinds and fields it was recorded with, as room
state does, so it plays after you change your schema.

## Clips

```ts
onMessage(room, connection, message) {
  if (message.type === "ace") room.clip({ seconds: 15, id: `ace-${room.tick}` });
},
```

`room.clip({ seconds })` keeps the last 1 to 60 seconds of the current
recording and returns the clip's ID. A room makes at most one clip every
10 seconds and 20 per replay. A clip is kept until you delete it, after
its replay has expired.

To clip from your backend, send the room a message and call `clip` in
`onRequest`, returning the ID:

```ts
onRequest(room, message) {
  if (message.type === "clip") return { clip: room.clip({ seconds: message.seconds }) };
},
```

## Downloading

Your backend lists a room's replays and clips and downloads their files
with the project's API key:

```sh
curl -H "Authorization: Bearer $IRT_API_KEY" \
  https://api.irt.io/v1/projects/my-game/rooms/match-42/replays
curl -H "Authorization: Bearer $IRT_API_KEY" -o match.irtr \
  https://api.irt.io/v1/projects/my-game/rooms/match-42/replays/match-42/file
```

The listing says when each replay started and ended, its ticks, size and
when it expires. A file grows while its replay records. Serve it to the
players you choose, from your own backend or storage.

## Playing back

```ts
import { loadReplay } from "@irt/client/replay";
import { Ball, schema } from "./schema.js";

const bytes = new Uint8Array(await (await fetch("/my-backend/replays/match-42")).arrayBuffer());
const replay = loadReplay(bytes, { schema });

replay.entities.onEnter(Ball, (b) => scene.add(b.id));
replay.entities.onChange(Ball, (b) => scene.move(b.id, b.x, b.y));
replay.entities.onLeave(Ball, (b) => scene.remove(b.id));
replay.on("message", (m) => feed.push(m));

replay.play();         // at the room's tick rate
replay.speed = 2;      // twice as fast
replay.pause();
replay.seek(1200);     // any tick from replay.firstTick to replay.lastTick
replay.step();         // the next tick; false at the end
```

- `replay.entities` is an `EntityStore`, as `room.entities` is on a live
  client, and `replay.state` and `replay.presence` work the same way.
  Entities are read-only.
- A seek starts from the full state at or before the tick, so it's fast
  anywhere in a replay. Entities shown before leave and those at the tick
  enter, without the changes between. Recorded messages fire only while
  playing through their tick.
- With `schema`, a kind whose fields are as recorded is your kind, so
  `entities.all(Ball)` finds its entities. A kind you've changed since is
  read from the replay: `replay.kind("ball")` returns it.
- `replay.privateOf(player)` is a player's private state, when the replay
  recorded it.
- A clip loads the same way and starts at its first second.

`@irt/client/replay` is a separate entry, so a game that doesn't play
replays doesn't carry it.

## Testing

`@irt/testing` keeps replays in memory:

```ts
const room = testRoom(game);
const alice = await room.connect({ player: "alice" });
await room.host.tick(100);
const replay = room.replay(room.live.recording!);
replay.seek(50);
expect([...replay.entities.all(Ball)]).toHaveLength(3);
```

`room.replays()` and `room.clips()` list IDs, and `room.clip(id)` loads
a clip. `irt dev` keeps replays on disk next to room state.

## Retention and cost

A replay is deleted `rooms.replayRetention` after it ends: 7 days by
default, from 1 hour to 30 days, set in `irt.config.ts` or the
dashboard. A room keeps its newest 20 replays and 100 clips. Deleting a
room through the API deletes its replays and clips; a room deleted by
its own retention keeps them until they expire or you delete them.

Replays and clips are billed as storage, by what IRT writes, never by
anything your room reports. See [Limits](/docs/limits#replays-and-clips).
