Recording¶
Room recording is available in beta. The feature lets users record their sessions; when a recording is complete, the room owner receives an email with a download link. Recordings are deleted automatically after RECORDING_EXPIRATION_DAYS if set.
Meet uses LiveKit Egress to capture room audio and video and upload the result to S3-compatible storage.
Known limitations:
- Recording layout is not configurable from the frontend. Egress captures the active speaker and any shared screens by default
- Shareable links with an embedded video player are not yet supported
How it works¶
sequenceDiagram
participant User
participant Frontend as Frontend (React)
participant Backend as Django Backend
participant LiveKit as LiveKit API
participant Egress as LiveKit Egress
participant Storage as MinIO / S3
participant Email as Email Service
User->>Frontend: Click start recording
Frontend->>Backend: POST /api/v1.0/rooms/{id}/start-recording/
Backend->>LiveKit: Create egress request
LiveKit->>Egress: Start room composite egress
Egress->>Storage: Join room and capture audio/video
LiveKit-->>Backend: Return egress_id
Backend-->>Frontend: HTTP 201: Recording started
Frontend->>Frontend: Notify other participants
Note over Egress,Storage: Recording in progress…
User->>Frontend: Click stop recording
Frontend->>Backend: POST /api/v1.0/rooms/{id}/stop-recording/
Backend->>LiveKit: Stop egress request
LiveKit->>Egress: Stop recording
Egress->>Storage: Upload recorded file
Storage->>Backend: Storage event webhook
Backend->>Email: Send notification to room owner
Email->>User: Email with download link
User->>Frontend: Navigate to /recording/{id}
Requirements¶
- A running LiveKit Egress server
- S3-compatible object storage with bucket event notifications (MinIO recommended)
- SMTP service for email notifications to room owners
- LiveKit webhook configured to reach the Meet backend
MinIO is strongly recommended. Other S3 providers may not support bucket event notifications out of the box. This dependency is planned to be refactored in a future release.
Docker Compose setup¶
Step 1: Configure MinIO¶
MinIO provides S3-compatible storage with the bucket event notifications Meet requires.
Add to your compose.yml:
minio:
image: minio/minio:latest
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin123
volumes:
- minio_data:/data
networks:
- proxy # must be on proxy to reach the reverse proxy for webhook delivery
- internal
volumes:
minio_data:
Step 2: Initialize the bucket¶
Create a one-time init container to set up the bucket and public access:
minio-init:
image: minio/mc:latest
restart: "no"
depends_on:
- minio
entrypoint: ["/bin/sh", "-c"]
command:
- "sleep 5 && mc alias set myminio http://minio:9000 minioadmin minioadmin123 && mc mb myminio/meet-media-storage --ignore-existing && mc anonymous set download myminio/meet-media-storage"
networks:
- internal
Note: The command must be a single-element list, not a YAML
>block. Usingcommand: >with a multi-line block causes theminio/mcimage to split arguments incorrectly, resulting in asleep: missing operanderror.
Run the init container:
Step 3: Configure bucket event notifications¶
MinIO must notify the Meet backend when a recording file is uploaded. Complete these three sub-steps in order.
1. Register the webhook target (replace <project> with your Compose project name, typically the directory name):
docker run --rm --network <project>_internal --entrypoint /bin/sh minio/mc:latest -c \
"mc alias set myminio http://minio:9000 minioadmin minioadmin123 && \
mc admin config set myminio notify_webhook:recordings endpoint='https://meet.example.com/api/v1.0/recordings/storage-hook/' queue_limit=10"
2. Restart MinIO (the webhook target is only active after a restart):
3. Subscribe the bucket to the webhook:
docker run --rm --network <project>_internal --entrypoint /bin/sh minio/mc:latest -c \
"mc alias set myminio http://minio:9000 minioadmin minioadmin123 && \
mc event add myminio/meet-media-storage arn:minio:sqs::recordings:webhook --event put --prefix recordings/"
Step 4: Set up LiveKit Egress¶
Create livekit-egress.yaml:
api_key: meetapikey
api_secret: your-livekit-api-secret
ws_url: ws://livekit:7880
redis:
address: redis:6379
s3:
access_key: minioadmin
secret: minioadmin123
endpoint: http://minio:9000
bucket: meet-media-storage
region: us-east-1 # required for S3-compatible storage
force_path_style: true # required for MinIO
cpu_cost:
room_composite_cpu_cost: 1.5
track_composite_cpu_cost: 1.0
track_cpu_cost: 0.5
cpu_costis required on servers with fewer than 4 CPU cores. LiveKit Egress defaults toroom_composite_cpu_cost: 4.0. If your server reports fewer available CPUs (common on VPS or shared hosting), Egress will refuse to start recordings with the errorno response from servers. Lowerroom_composite_cpu_costto match your available resources.The
redissection is mandatory. Egress uses Redis for communication with LiveKit Server. Without it, Egress will crash with a nil pointer dereference on startup.
Add to your compose.yml:
livekit-egress:
image: livekit/egress:latest
restart: unless-stopped
environment:
EGRESS_CONFIG_FILE: /livekit-egress.yaml
volumes:
- ./livekit-egress.yaml:/livekit-egress.yaml:ro
depends_on:
- redis
- minio
- livekit
networks:
- internal
Step 5: Configure the Meet backend¶
Add to your .env:
RECORDING_ENABLE=True
AWS_S3_ENDPOINT_URL=http://minio:9000
AWS_S3_ACCESS_KEY_ID=minioadmin
AWS_S3_SECRET_ACCESS_KEY=minioadmin123
AWS_STORAGE_BUCKET_NAME=meet-media-storage
AWS_S3_SECURE_ACCESS=False
RECORDING_STORAGE_EVENT_ENABLE=True
RECORDING_ENABLE_STORAGE_EVENT_AUTH=False # simplest setup; set True and configure RECORDING_STORAGE_EVENT_TOKEN for token auth
RECORDING_DOWNLOAD_BASE_URL=https://meet.example.com/recording
# DJANGO_ALLOWED_HOSTS - ensure it includes the public domain and internal service names
DJANGO_ALLOWED_HOSTS=meet.example.com,backend,localhost
RECORDING_DOWNLOAD_BASE_URLmust include the/recordingpath. The frontend expects download links athttps://meet.example.com/recording/<uuid>. Using the bare domain sends users to a page that treats the UUID as a room code.Email is required for recording downloads. When a recording is ready, Meet sends the room owner an email with the download link. Without SMTP, users will never receive this notification. Configure SMTP in the same
.env:DJANGO_EMAIL_HOST=smtp.example.com DJANGO_EMAIL_PORT=587 DJANGO_EMAIL_HOST_USER=meet@example.com DJANGO_EMAIL_HOST_PASSWORD=<password> DJANGO_EMAIL_USE_TLS=True DJANGO_EMAIL_FROM=meet@example.comIf SMTP is not configured, administrators can find recording download links in the Django admin panel at
/admin/ → Core → Recordings.
Restart the backend:
Verify recording is enabled:
Expected output:
{
"is_enabled": true,
"available_modes": ["screen_recording", "transcript"],
"expiration_days": null,
"max_duration": null
}
Step 6: Update nginx routing for recording downloads¶
Add a minio_backend upstream to the template (alongside the existing upstreams):
Add the /media/ location inside the server { } block:
location /media/ {
proxy_pass http://minio_backend/meet-media-storage/;
proxy_set_header Host minio;
}
Your template now includes the MinIO upstream and /media/ location. Reload the frontend container:
This routes
https://meet.example.com/media/recordings/<uuid>.mp4through the inner nginx to MinIO. MinIO stays on the internal Docker network and is never directly exposed to the internet.
Step 7: Configure LiveKit webhook¶
LiveKit notifies Meet when a recording starts, stops, or fails via a signed webhook. The webhook URL must be the public HTTPS URL: Django's SecurityMiddleware redirects plain HTTP to HTTPS, causing internal requests to fail.
Add to livekit-server.yaml:
Recreate affected containers:
Verify webhooks arrive by watching backend logs after starting a recording:
A successful webhook shows: POST /api/v1.0/rooms/webhooks-livekit/ HTTP/1.1" 200
Kubernetes setup¶
Recording in Kubernetes uses the same components but wired differently: the /media/ download proxy is handled by ingressMedia via nginx-ingress auth subrequests rather than an nginx-routing.conf block.
Step 1: Deploy MinIO¶
Deploy MinIO in the meet namespace using the MinIO Helm chart, or use an external S3-compatible service:
helm repo add minio https://charts.min.io/
helm install minio minio/minio \
--namespace meet \
--set rootUser=minioadmin \
--set rootPassword=minioadmin123 \
--set mode=standalone \
--set persistence.size=50Gi
Step 2: Initialize the bucket and webhook¶
Run the minio/mc commands from a temporary pod, identical to the Docker Compose setup:
kubectl -n meet run minio-init --image=minio/mc --restart=Never --rm -it -- /bin/sh -c "
mc alias set myminio http://minio:9000 minioadmin minioadmin123 && \
mc mb myminio/meet-media-storage --ignore-existing && \
mc anonymous set download myminio/meet-media-storage && \
mc admin config set myminio notify_webhook:recordings \
endpoint='https://meet.example.com/api/v1.0/recordings/storage-hook/' \
queue_limit=10 && \
mc admin service restart myminio && \
sleep 5 && \
mc event add myminio/meet-media-storage arn:minio:sqs::recordings:webhook \
--event put --prefix recordings/
"
Step 3: Deploy LiveKit Egress¶
Egress is not part of the Meet Helm chart. Deploy it as a Kubernetes Deployment with a ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: livekit-egress-config
namespace: meet
data:
livekit-egress.yaml: |
api_key: meetapikey
api_secret: your-livekit-secret
ws_url: ws://livekit-server:7880
redis:
address: redis-master:6379
s3:
access_key: minioadmin
secret: minioadmin123
endpoint: http://minio:9000
bucket: meet-media-storage
region: us-east-1
force_path_style: true
cpu_cost:
room_composite_cpu_cost: 2.0
track_composite_cpu_cost: 1.0
track_cpu_cost: 0.5
apiVersion: apps/v1
kind: Deployment
metadata:
name: livekit-egress
namespace: meet
spec:
replicas: 1
selector:
matchLabels:
app: livekit-egress
template:
metadata:
labels:
app: livekit-egress
spec:
containers:
- name: livekit-egress
image: livekit/egress:latest
env:
- name: EGRESS_CONFIG_FILE
value: /config/livekit-egress.yaml
volumeMounts:
- name: config
mountPath: /config
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 2Gi
volumes:
- name: config
configMap:
name: livekit-egress-config
Step 4: Configure the backend¶
Add to backend.envVars in your values.yaml:
backend:
envVars:
RECORDING_ENABLE: "True"
AWS_S3_ENDPOINT_URL: "http://minio:9000"
AWS_S3_ACCESS_KEY_ID: "minioadmin"
AWS_S3_SECRET_ACCESS_KEY: "minioadmin123"
AWS_STORAGE_BUCKET_NAME: "meet-media-storage"
AWS_S3_SECURE_ACCESS: "False"
RECORDING_STORAGE_EVENT_ENABLE: "True"
RECORDING_ENABLE_STORAGE_EVENT_AUTH: "False"
RECORDING_DOWNLOAD_BASE_URL: "https://meet.example.com/recording"
DJANGO_ALLOWED_HOSTS: "meet.example.com"
RECORDING_DOWNLOAD_BASE_URLmust include the/recordingpath. See Step 5 in the Docker Compose guide for why.Email is required. Without SMTP, users never receive download links. Add
DJANGO_EMAIL_*variables tobackend.envVars. See Step 5 above for the variable names.
Apply the updated chart:
Step 5: Enable ingressMedia¶
ingressMedia proxies /media/(.*) requests to MinIO via nginx-ingress auth subrequests. The backend authenticates each request before MinIO serves the file.
ingressMediarequires nginx-ingress. These annotations are nginx-ingress specific and do not work with other ingress controllers.
Add to values.yaml:
ingressMedia:
enabled: true
className: nginx
host: meet.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-url: "https://meet.example.com/api/v1.0/recordings/media-auth/"
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: "minio.meet.svc.cluster.local:9000"
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Content-Security-Policy "default-src 'none'" always;
tls:
enabled: true
secretName: meet-tls
serviceMedia:
host: minio.meet.svc.cluster.local
port: 9000
Apply:
Verify recording is enabled:
Configuring recording encoding¶
By default, LiveKit Egress records with the built-in H264_720P_30 preset: 1280x720 at 30 fps, 3000 kbps H.264 video and 128 kbps AAC audio, roughly 1.4 GB per hour. The RECORDING_ENCODING_* variables let you override this preset.
Values are passed directly to LiveKit's EncodingOptions.advanced and map to GStreamer pipeline settings:
| Setting | GStreamer element | Property |
|---|---|---|
RECORDING_ENCODING_WIDTH/HEIGHT |
capsfilter | video/x-raw,width=W,height=H |
RECORDING_ENCODING_FRAMERATE |
capsfilter | framerate=F/1 |
RECORDING_ENCODING_VIDEO_BITRATE_KBPS |
x264enc |
bitrate=kbps |
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S |
x264enc |
key-int-max = interval x fps |
RECORDING_ENCODING_AUDIO_BITRATE_KBPS |
faac |
bitrate = kbps x 1000 |
Reference profiles (30-minute size estimates; actual sizes vary with content):
| Profile | Resolution | FPS | Video kbps | Audio kbps | ~size / 30 min | CPU vs default | Best for |
|---|---|---|---|---|---|---|---|
| Default (preset) | 1280x720 | 30 | 3000 | 128 | ~690 MB | 100% | Unchanged LiveKit behaviour |
| Balanced | 1280x720 | 20 | 1000 | 96 | ~240 MB | ~67% | Mixed content, moderate motion |
| Low CPU / small file * | 1280x720 | 15 | 600 | 64 | ~150 MB | ~50% | Talking-head meetings + occasional slides |
| Slide-heavy | 1280x720 | 15 | 900 | 64 | ~210 MB | ~55% | Frequent screen sharing, dense slides |
| Minimum CPU | 960x540 | 15 | 500 | 64 | ~125 MB | ~30% | Voice-first meetings, readable text not required |
| Audio-heavy fallback | 1280x720 | 10 | 400 | 96 | ~110 MB | ~35% | Long webinars, low motion |
- Recommended starting point for typical Meet usage.
Example: Low CPU / small file profile:
RECORDING_ENCODING_ENABLED=True
RECORDING_ENCODING_WIDTH=1280
RECORDING_ENCODING_HEIGHT=720
RECORDING_ENCODING_FRAMERATE=15
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600
RECORDING_ENCODING_AUDIO_BITRATE_KBPS=64
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
Caveats:
- Screen-share readability. At 720p, text legibility breaks down below ~40 kbits/frame (
bitrate / framerate). The Low CPU profile (600 kbps / 15 fps = 40 kbits/frame) sits at that threshold. The same 600 kbps at 30 fps gives only 20 kbits/frame and visibly blurs dense slides. Lowering framerate is a more screen-share-friendly lever than lowering bitrate. For deck-heavy meetings, use the Slide-heavy profile (900 / 15 = 60 kbits/frame). - Motion. The
veryfastx264 preset is set by LiveKit and cannot be overridden. Low-bitrate settings show more artefacts on fast motion than an offline re-encode would. - Audio. AAC at 64 kbps stereo is transparent for voice but compresses music noticeably. Keep 128 kbps if meetings include music playback.
- Codec. H.264 MAIN is hardcoded. Switching to HEVC or VP9 would increase egress CPU cost 2x-5x.
Monitoring egress workers¶
Install livekit-cli to inspect active recording sessions:
This lets you verify which recordings are in progress, troubleshoot egress issues, and confirm recordings are being processed correctly.
Full configuration reference¶
| Variable | Type | Default | Description |
|---|---|---|---|
RECORDING_ENABLE |
Boolean | False |
Enable the recording feature |
RECORDING_OUTPUT_FOLDER |
String | "recordings" |
Folder/prefix in object storage |
RECORDING_STORAGE_EVENT_ENABLE |
Boolean | False |
Enable storage event webhook handling |
RECORDING_ENABLE_STORAGE_EVENT_AUTH |
Boolean | True |
Require token auth on storage webhook |
RECORDING_STORAGE_EVENT_TOKEN |
Secret | None |
Token for storage webhook auth |
RECORDING_DOWNLOAD_BASE_URL |
String | - | Base URL for download links (must include /recording) |
RECORDING_EXPIRATION_DAYS |
Integer | None |
Days before recordings expire; should match bucket lifecycle policy |
RECORDING_MAX_DURATION |
Integer | None |
Max recording duration in milliseconds |
RECORDING_ENCODING_ENABLED |
Boolean | False |
Use custom encoding instead of built-in preset |
RECORDING_ENCODING_WIDTH |
Integer | 1280 |
Video width in pixels |
RECORDING_ENCODING_HEIGHT |
Integer | 720 |
Video height in pixels |
RECORDING_ENCODING_FRAMERATE |
Integer | 30 |
Video framerate (fps) |
RECORDING_ENCODING_VIDEO_BITRATE_KBPS |
Integer | 3000 |
H.264 video bitrate |
RECORDING_ENCODING_AUDIO_BITRATE_KBPS |
Integer | 128 |
AAC audio bitrate |
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S |
Float | 4.0 |
Keyframe interval in seconds |
RECORDING_EVENT_PARSER_CLASS |
String | "core.recording.event.parsers.MinioParser" |
Class for parsing storage events. Use MinioParser for MinIO; use core.recording.event.parsers.S3Parser for generic S3-compatible providers (v1.17.0+). |
Testing the full recording flow¶
- Start a meeting and click Record
- Speak for 30 seconds, then click Stop recording
- Check MinIO for the file:
- Check backend logs for the storage webhook:
docker compose logs backend | grep storage-hook - The room owner should receive an email with a download link
Alternative S3 providers¶
MinIO is recommended for self-hosted single-server deployments. Other S3-compatible providers work with Meet using the same environment variables:
- AWS S3: production-scale, managed
- OVH Object Storage: European hosting
- Scaleway Object Storage: French hosting
- Ceph RGW: enterprise-grade, self-hosted
Note that some providers may not support bucket event notifications; in that case, recording downloads must be accessed through the Django admin panel.