Recording API
Live class recording — start, stop, and Jibri webhook
OptiLearn embeds Jitsi Meet for live classes (Sprint ε Track 2). Recording is wired through Jibri (Jitsi Broadcasting Infrastructure). The OptiLearn endpoints orchestrate the host-facing UI; provisioning Jibri itself is ops work — see Jibri Recording Setup.
Wrapper: src/lib/jitsi-recording.ts. Webhook handler verifies an HMAC signature.
POST /api/live-sessions/[id]/recording/start
Host-only. Tells Jibri to start recording the session's room.
Pre-conditions
| Check | Failure |
|---|---|
| Session exists in caller's institution | 404 |
Caller is the host (canManageSession) | 403 |
Session status is LIVE (recording before /start would target a non-existent room) | 409 |
Session has a roomName | 409 |
Not already in RECORDING or PENDING (idempotent — returns existing state) | 200 with alreadyRecording: true |
Flow
- Optimistically flip
recordingStatus = PENDING— prevents two rapid clicks firing two Jibri starts - Call
startJibriRecording({ roomName, callbackUrl, sessionId }) - On success: flip
recordingStatus = RECORDING, stamprecordingStartedAt, clear stale stop fields - On failure: roll back to the prior status
Response
{
"data": {
"recordingStatus": "RECORDING",
"recordingStartedAt": "2026-04-15T14:02:00Z"
}
}
When Jibri isn't provisioned (env vars missing or meet server returns 404), responds 503 with the friendly message so the UI can show "contact your administrator" instead of a generic 500.
Callback URL
Built from NEXTAUTH_URL (preferred) or fallback https://learn.opticrm.app:
<baseUrl>/api/webhooks/jitsi-recording
Jibri must be able to POST back to this URL — for local dev that means a tunnel (ngrok / cloudflared / etc).
POST /api/live-sessions/[id]/recording/stop
Host-only counterpart to /start. Tells Jibri to stop, flips the session to PROCESSING and stamps recordingStoppedAt. The actual recordingUrl arrives later via the webhook.
Idempotent — stopping a non-RECORDING/PENDING session is a no-op (returns alreadyStopped: true).
Failure mode
If Jibri is unreachable on stop, the local state moves to FAILED (rather than leaving a stuck RECORDING red dot) so the host can recover. Returns 503 with the friendly message.
Response
{
"data": {
"recordingStatus": "PROCESSING",
"recordingStoppedAt": "2026-04-15T15:07:00Z"
}
}
POST /api/webhooks/jitsi-recording
Public webhook endpoint Jibri calls when a recording finishes uploading. Signature-verified — no session cookie.
Authentication
HMAC-SHA256 over the raw request body, signed with JITSI_RECORDING_WEBHOOK_SECRET. The signature header may be plain hex or sha256= prefixed (GitHub-style):
X-Jibri-Signature: <hex> # or
X-Jibri-Signature: sha256=<hex> # also accepted
X-Jitsi-Signature: <hex> # alias
If JITSI_RECORDING_WEBHOOK_SECRET is unset on the server, the endpoint refuses everything with 503 — refusing rather than defaulting prevents anyone on the public internet from posting arbitrary recording URLs.
Payload
{
"event": "recording_uploaded", // or "recording_failed"
"sessionId": "<LiveSession.id>", // echoed back from /start
"roomName": "optilearn-<slug>-<id>", // safety net
"recordingUrl": "https://...", // R2 URL, only on success
"duration": 1834 // seconds, optional
}
Handling
recording_failed→ setrecordingStatus = FAILED,recordingStoppedAt = now. Ack 200.recording_uploaded→ storerecordingUrl, setrecordingStatus = READY, storerecordingDuration. Then fanLIVE_CLASS_RECORDING_READYout to every active/completed enrollment vianotificationService.sendBulk. Fan-out is fire-and-forget — the webhook ack does not wait on email/FCM.roomNamemismatch → 400 (misrouted webhook; refuse rather than corrupt state)- Unknown session → 200 with
ignored: true(don't 404; Jibri retries on non-2xx) - Bad signature → 401
Response
{ "data": { "ok": true, "status": "READY" } }
Recording status state machine
NULL ── start ──▶ PENDING ── start succeeds ──▶ RECORDING
▲ │ │
│ ├─ start fails ────────────────┘
│ │
│ ▼
│ (rollback)
│ │
│ stop ──┘
│ ▼
│ PROCESSING
│ │
│ webhook recording_uploaded ──▶ READY
│ webhook recording_failed ──▶ FAILED
│ stop while Jibri unreachable ─▶ FAILED
Required environment variables
Both must be set on the OptiLearn server for recording to work end-to-end:
| Variable | Purpose |
|---|---|
JITSI_RECORDING_API_TOKEN | Bearer token used by startJibriRecording / stopJibriRecording to call the Jibri-enabled meet server |
JITSI_RECORDING_WEBHOOK_SECRET | HMAC secret for verifying inbound Jibri callbacks |
Without these, /start returns 503 and the webhook refuses 503. See Jibri Recording Setup for ops.
Related
- Jibri Recording Setup — provisioning the sidecar
- Notifications System —
LIVE_CLASS_RECORDING_READYfan-out