Recording
Optional start/stop. Per-participant MP4 on S3.
Recording is a lossy fork, not a third peer. Rill copies live RTP (and reassembled data messages) to rill-record with a non-blocking try_send. The sidecar writes a disk buffer, muxes MP4, uploads S3, and POSTs your app. The live SFU loop does not wait on FFmpeg or S3.
If rill-record is omitted or down, the call continues. create({ recording: true }) still returns 201 with recording: false. Later recordingStart() returns recording_unavailable.
Still two people. Destroying the call stops an active recording. The public demo (compose.site.yaml) does not run recording.
Rill SFU --try_send--> rill-record --> /recordings (buffer)
| |
| +-- FFmpeg mux
| +-- S3 PUT
+-- HMAC POST recording.stopped
Create vs start/stop
import { Rill } from "@rill/server";
const rill = new Rill({
url: "https://rill.example.com",
apiKey: process.env.RILL_API_KEY!,
apiSecret: process.env.RILL_API_SECRET!,
});
const call = await rill.calls.create({ recording: true });
if (!call.recording) {
// Sidecar down. The call still exists.
}
await call.recordingStart();
await call.recordingStop();
await call.end();curl -s -X POST http://127.0.0.1:7880/v1/calls/$CALL/recording/start \
-H "Authorization: Bearer devkey"
curl -s -X POST http://127.0.0.1:7880/v1/calls/$CALL/recording/stop \
-H "Authorization: Bearer devkey"docker compose up already starts rill-record and Garage. HMAC for recording.stopped is configured on the sidecar (RILL_RECORD_WEBHOOK_SECRET), not the SFU. Same header scheme as join/leave — Webhooks.
Local compose
Contributors can run mise run record-demo (or bun docs/examples/recording-local.ts) after compose is up. Signed GET URLs use Garage at http://127.0.0.1:3900. The try-it site on :3000 does not start recording.
Files
After stop, each participant with camera+mic is one MP4 (H.264 + AAC) that plays in Chrome and desktop Safari <video>. Timing follows the live media clocks, not a fake 30 fps. Audio-only is an AAC MP4. Screen share is {participant}-screen.mp4. Eligible data labels are {participant}-{label}.jsonl (application/x-ndjson); empty files are omitted. Dots in labels stay dots (alice-a.b.jsonl ≠ alice-a_b.jsonl). Each complete data-channel message is one JSONL line (t in microseconds, encoding utf8 or base64, payload). Chunk rrweb snapshots in the app (≤128 KiB, 60 KiB is safe) so live relay and JSONL stay in sync. Labels starting with _ are SDK-internal and are not recorded. No composite of both people. No RTMP. No download HTTP on the SFU.
Objects land in Garage (local) or S3 (production). Disk under /recordings is a buffer and is cleared after upload. Dumps are not the playable format. If the sidecar restarts mid-call, it muxes leftover dumps on startup and may POST recording.stopped again with the same id ({callId}-{startedAt}); tolerate a duplicate webhook if the crash happened after mux but before the first POST.
Webhook
Set on rill-record (not the SFU): RILL_RECORD_WEBHOOK_URL and RILL_RECORD_WEBHOOK_SECRET. Same HMAC as Rill join/leave: X-Rill-Timestamp + X-Rill-Signature. Signed GET URLs last 24 hours. Rill’s join/leave webhook set does not include recording events.
{
"type": "recording.stopped",
"callId": "<ulid>",
"startedAt": "20260820T120000Z",
"stoppedAt": "20260820T121500Z",
"files": [
{
"participantId": "alice",
"kind": "av",
"bucket": "rill",
"key": "rill/<call>/<started>/alice.mp4",
"url": "http://127.0.0.1:3900/rill/...",
"contentType": "video/mp4"
},
{
"participantId": "alice",
"kind": "data",
"label": "chat",
"bucket": "rill",
"key": "rill/<call>/<started>/alice-chat.jsonl",
"url": "http://127.0.0.1:3900/rill/...",
"contentType": "application/x-ndjson"
}
]
}kind is av, screen, or data. Data files include label. If mux fails, that entry includes error and may omit url. The live call does not wait on S3.
Production
Point the sidecar at real S3. Keep bucket keys in the recorder env, never the SFU .env. Socket must match: RILL_RECORD_SOCKET on both processes (127.0.0.1:7420 or a unix path).
| Variable | Role |
|---|---|
RILL_RECORD_SOCKET | Must match the SFU |
RILL_RECORD_DIR | Disk buffer |
RILL_RECORD_S3_BUCKET | Bucket |
RILL_RECORD_S3_PREFIX | Object prefix (default rill/) |
RILL_RECORD_S3_ENDPOINT | Garage/R2 origin; omit for AWS |
RILL_RECORD_S3_REGION | Default us-east-1 (or auto with an endpoint) |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | Sidecar only |
RILL_RECORD_WEBHOOK_URL / RILL_RECORD_WEBHOOK_SECRET | Completion POST |
If this service is down, POST .../recording/start fails; the call does not.