Recordings and replays
Record matches on your server, save them and read them back.
A recording is a compact binary file (.ball2drep) with everything needed to replay a match exactly: the stadium, every input and command, and checkpoints that verify playback.
Record a match
import { writeFile } from 'node:fs/promises';
room.onGameStart = () => room.startRecording();
room.onGameStop = async () => {
const blob = room.stopRecording();
if (!blob) return;
const bytes = Buffer.from(await blob.arrayBuffer());
await writeFile(`recordings/${Date.now()}.ball2drep`, bytes);
};startRecording() and stopRecording() are synchronous. stopRecording() returns the file directly.
Recordings the room finishes
Some events end a recording for you. The room then delivers it to onRecordingComplete and keeps it in lastRecording:
| Reason | When |
|---|---|
Recording limit reached | One hour of play, 30 MB, or the command limit |
Stadium changed | Before a new stadium loads |
Room closed | When the room closes, including close() |
Handle onRecordingComplete whenever you record, so you don't lose the match that was in progress when the room closed:
room.onRecordingComplete = async (blob, reason) => {
await writeFile(
`recordings/${Date.now()}-auto.ball2drep`,
Buffer.from(await blob.arrayBuffer()),
);
};Read a recording
readReplay() decodes and verifies a file without a room:
import { readFile } from 'node:fs/promises';
import { readReplay } from 'ball2d/node';
const replay = await readReplay(
new Blob([await readFile('final.ball2drep')]),
);
const seconds = (replay.end - replay.initial.tick) / 60;
console.log(
`${seconds.toFixed(0)} s, ${replay.commands.length} commands`,
);The decoded Replay holds the stadium source, the starting state, the commands and the roster changes.
A recording can be read only by the engine build that made it. Keep the SDK version you recorded with if you need to read old files later.